mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
Merge branch 'feat/apu' into dev
This commit is contained in:
50 files changed
+9153
-44
No files matched your search
@@ -43,6 +43,7 @@ HAVE_SRAM_MACRO ?= $(if $(filter ICS55,$(PDK)),NO,YES)
|
|||||||
SRAM_SIZE_KIB ?= $(if $(filter ICS55,$(PDK)),128,32)
|
SRAM_SIZE_KIB ?= $(if $(filter ICS55,$(PDK)),128,32)
|
||||||
PDK_BEHAV ?= NO
|
PDK_BEHAV ?= NO
|
||||||
HAVE_SVA ?= NO
|
HAVE_SVA ?= NO
|
||||||
|
APU_ENABLE_P7 ?= NO
|
||||||
HAVE_HP ?= YES
|
HAVE_HP ?= YES
|
||||||
HP_CONFIG ?= rv32imafdc_zicbom_max
|
HP_CONFIG ?= rv32imafdc_zicbom_max
|
||||||
BUILD_RELEASE ?= NO
|
BUILD_RELEASE ?= NO
|
||||||
@@ -101,7 +102,7 @@ JOBS ?= $(shell count=$$(nproc 2>/dev/null || printf '1'); \
|
|||||||
if [ "$$count" -gt "$(MAX_JOBS)" ]; then printf '%s' '$(MAX_JOBS)'; \
|
if [ "$$count" -gt "$(MAX_JOBS)" ]; then printf '%s' '$(MAX_JOBS)'; \
|
||||||
else printf '%s' "$$count"; fi)
|
else printf '%s' "$$count"; fi)
|
||||||
LOCAL_RTL_FILES ?=
|
LOCAL_RTL_FILES ?=
|
||||||
CONFIG_KEY_VARS := SOC MINI_MODE PDK HAVE_PLL HAVE_SRAM_IF HAVE_SRAM_MACRO SRAM_SIZE_KIB PDK_BEHAV HAVE_SVA \
|
CONFIG_KEY_VARS := SOC MINI_MODE PDK HAVE_PLL HAVE_SRAM_IF HAVE_SRAM_MACRO SRAM_SIZE_KIB PDK_BEHAV HAVE_SVA APU_ENABLE_P7 \
|
||||||
HAVE_HP HP_CONFIG BUILD_RELEASE JTAG_IDCODE EXT_CLK_HZ AUD_CLK_HZ CLINT_TIMEBASE_HZ MGMT_CPU_CLK_HZ \
|
HAVE_HP HP_CONFIG BUILD_RELEASE JTAG_IDCODE EXT_CLK_HZ AUD_CLK_HZ CLINT_TIMEBASE_HZ MGMT_CPU_CLK_HZ \
|
||||||
ISA HAVE_CSR APP LINK_TYPE COREMARK_MODE RTL_TOP FIRMWARE_NAME
|
ISA HAVE_CSR APP LINK_TYPE COREMARK_MODE RTL_TOP FIRMWARE_NAME
|
||||||
VARIANT_ID := $(strip $(shell $(VCS_SHELL_PYTHON) $(ROOT_PATH)/scripts/config_key.py \
|
VARIANT_ID := $(strip $(shell $(VCS_SHELL_PYTHON) $(ROOT_PATH)/scripts/config_key.py \
|
||||||
@@ -129,6 +130,9 @@ APU_P5_REFERENCE_DIR := $(VARIANT_ROOT)/apu/reference
|
|||||||
APU_P5_CORPUS_MANIFEST := $(APU_P5_DIR)/corpus-manifest.json
|
APU_P5_CORPUS_MANIFEST := $(APU_P5_DIR)/corpus-manifest.json
|
||||||
APU_P5_CORPUS_RTL_DIR := $(APU_P5_DIR)/corpus-rtl
|
APU_P5_CORPUS_RTL_DIR := $(APU_P5_DIR)/corpus-rtl
|
||||||
APU_P7_DIR := $(VARIANT_ROOT)/apu/kws
|
APU_P7_DIR := $(VARIANT_ROOT)/apu/kws
|
||||||
|
APU_P7_MODEL := $(APU_P7_DIR)/apu-p7.apum
|
||||||
|
APU_P7_MODEL_MANIFEST := $(APU_P7_DIR)/apu-p7-manifest.json
|
||||||
|
APU_P7_KWS_TFLITE := $(CACHE_ROOT)/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting/trained_models/kws_ref_model.tflite
|
||||||
HP_LINUX_BUILD_DIR := $(VARIANT_ROOT)/hp-linux
|
HP_LINUX_BUILD_DIR := $(VARIANT_ROOT)/hp-linux
|
||||||
HP_LINUX_STAMP := $(HP_LINUX_BUILD_DIR)/images/.stamp
|
HP_LINUX_STAMP := $(HP_LINUX_BUILD_DIR)/images/.stamp
|
||||||
HP_BOOT_BUNDLE_NAME ?= retrosoc_hp_linux
|
HP_BOOT_BUNDLE_NAME ?= retrosoc_hp_linux
|
||||||
@@ -143,6 +147,12 @@ HP_SMOKE_BUNDLE_NAME ?= retrosoc_hp_smoke
|
|||||||
HP_SMOKE_BUNDLE_BIN := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).bin
|
HP_SMOKE_BUNDLE_BIN := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).bin
|
||||||
HP_SMOKE_BUNDLE_HEX := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).hex
|
HP_SMOKE_BUNDLE_HEX := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).hex
|
||||||
HP_SMOKE_MANIFEST := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).json
|
HP_SMOKE_MANIFEST := $(SW_BUILD_DIR)/$(HP_SMOKE_BUNDLE_NAME).json
|
||||||
|
HP_APU_BUILD_DIR := $(VARIANT_ROOT)/hp-apu
|
||||||
|
HP_APU_STAMP := $(HP_APU_BUILD_DIR)/images/.stamp
|
||||||
|
HP_APU_BUNDLE_NAME ?= retrosoc_hp_apu
|
||||||
|
HP_APU_BUNDLE_BIN := $(SW_BUILD_DIR)/$(HP_APU_BUNDLE_NAME).bin
|
||||||
|
HP_APU_BUNDLE_HEX := $(SW_BUILD_DIR)/$(HP_APU_BUNDLE_NAME).hex
|
||||||
|
HP_APU_MANIFEST := $(SW_BUILD_DIR)/$(HP_APU_BUNDLE_NAME).json
|
||||||
HP_BUILDRT_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/buildroot-hp
|
HP_BUILDRT_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/buildroot-hp
|
||||||
HP_LINUX_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/linux-hp
|
HP_LINUX_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/linux-hp
|
||||||
HP_OPENSBI_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/opensbi-hp
|
HP_OPENSBI_ROOT := $(ROOT_PATH)/.cache/retrosoc/sources/opensbi-hp
|
||||||
@@ -173,7 +183,7 @@ VALID_PDK := ICS55 IHP130 SKY130 GF180
|
|||||||
VALID_BOOL := YES NO
|
VALID_BOOL := YES NO
|
||||||
VALID_HP_CONFIG := rv32imafdc_zicbom_max
|
VALID_HP_CONFIG := rv32imafdc_zicbom_max
|
||||||
VALID_ISA := RV32E RV32I RV32IM
|
VALID_ISA := RV32E RV32I RV32IM
|
||||||
VALID_APP := benchmark bringup ci_smoke coremark debug hp_boot shell xpi_flash_loader
|
VALID_APP := benchmark bringup ci_smoke coremark debug hp_boot shell xpi_flash_loader apu_release
|
||||||
VALID_LINK_TYPE := xip jtag_sram ld2_all_sram ld2_sram ld2_psram ld2_sdram
|
VALID_LINK_TYPE := xip jtag_sram ld2_all_sram ld2_sram ld2_psram ld2_sdram
|
||||||
VALID_COREMARK_MODE := quick standard
|
VALID_COREMARK_MODE := quick standard
|
||||||
VALID_SRAM_SIZE_KIB := 4 16 32 64 128
|
VALID_SRAM_SIZE_KIB := 4 16 32 64 128
|
||||||
@@ -195,6 +205,7 @@ $(call validate_value,HAVE_SRAM_MACRO,$(VALID_BOOL))
|
|||||||
$(call validate_value,SRAM_SIZE_KIB,$(VALID_SRAM_SIZE_KIB))
|
$(call validate_value,SRAM_SIZE_KIB,$(VALID_SRAM_SIZE_KIB))
|
||||||
$(call validate_value,PDK_BEHAV,$(VALID_BOOL))
|
$(call validate_value,PDK_BEHAV,$(VALID_BOOL))
|
||||||
$(call validate_value,HAVE_SVA,$(VALID_BOOL))
|
$(call validate_value,HAVE_SVA,$(VALID_BOOL))
|
||||||
|
$(call validate_value,APU_ENABLE_P7,$(VALID_BOOL))
|
||||||
$(call validate_value,HAVE_HP,$(VALID_BOOL))
|
$(call validate_value,HAVE_HP,$(VALID_BOOL))
|
||||||
$(call validate_value,HP_CONFIG,$(VALID_HP_CONFIG))
|
$(call validate_value,HP_CONFIG,$(VALID_HP_CONFIG))
|
||||||
$(call validate_value,BUILD_RELEASE,$(VALID_BOOL))
|
$(call validate_value,BUILD_RELEASE,$(VALID_BOOL))
|
||||||
@@ -317,6 +328,10 @@ ifeq ($(HAVE_HP), YES)
|
|||||||
DEF_LIST += +define+HAVE_HP
|
DEF_LIST += +define+HAVE_HP
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifeq ($(APU_ENABLE_P7), YES)
|
||||||
|
DEF_LIST += +define+APU_ENABLE_P7
|
||||||
|
endif
|
||||||
|
|
||||||
ifeq ($(MINI_MODE), PRODUCT)
|
ifeq ($(MINI_MODE), PRODUCT)
|
||||||
DEF_LIST += +define+MINI_PRODUCT
|
DEF_LIST += +define+MINI_PRODUCT
|
||||||
else
|
else
|
||||||
@@ -339,6 +354,7 @@ endif
|
|||||||
ifeq ($(SYNTH), YOSYS)
|
ifeq ($(SYNTH), YOSYS)
|
||||||
include physical/smoke/syn/yosys/yosys.mk
|
include physical/smoke/syn/yosys/yosys.mk
|
||||||
include physical/smoke/syn/yosys/ga2d_block.mk
|
include physical/smoke/syn/yosys/ga2d_block.mk
|
||||||
|
include physical/smoke/syn/yosys/apu_block.mk
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(STA), OPENSTA)
|
ifeq ($(STA), OPENSTA)
|
||||||
@@ -348,12 +364,13 @@ endif
|
|||||||
include physical/librelane/Makefile
|
include physical/librelane/Makefile
|
||||||
include physical/ecc/Makefile
|
include physical/ecc/Makefile
|
||||||
|
|
||||||
.PHONY: help config doctor setup setup-regression setup-mpw setup-vexiiriscv setup-clusterip setup-ip setup-pdk setup-app setup-apu-reference setup-apu-kws-reference apu-p5-bundle apu-p5-corpus setup-hp-linux hp-linux hp-bundle hp-linux-sim hp-smoke-bundle hp-smoke-sim \
|
.PHONY: help config doctor setup setup-regression setup-mpw setup-vexiiriscv setup-clusterip setup-ip setup-pdk setup-app setup-apu-reference setup-apu-kws-reference apu-p5-bundle apu-p5-corpus apu-p7-model setup-hp-linux hp-linux hp-bundle hp-linux-sim hp-smoke-bundle hp-smoke-sim hp-apu-bundle hp-apu-sim \
|
||||||
clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \
|
clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \
|
||||||
regress-smoke regress-rtl regress-pr regress-nightly sim-asm format format-check sw-format sw-format-check mk-format \
|
regress-smoke regress-rtl regress-pr regress-nightly sim-asm format format-check sw-format sw-format-check mk-format \
|
||||||
mk-format-check rtl-format rtl-format-check rtl-style-check rtl-migrate-connections rtl-migrate-names sw-policy-check sw-host-test \
|
mk-format-check rtl-format rtl-format-check rtl-style-check rtl-migrate-connections rtl-migrate-names sw-policy-check sw-host-test \
|
||||||
benchmark-report coremark-report \
|
benchmark-report coremark-report \
|
||||||
hp-performance-check \
|
hp-performance-check \
|
||||||
|
apu-block-filelist apu-block-synth apu-block-sta apu-block-report apu-block-evidence apu-block-clean \
|
||||||
pin-map check-pin-map soc-topology check-soc-topology user-extensions check-user-extensions \
|
pin-map check-pin-map soc-topology check-soc-topology user-extensions check-user-extensions \
|
||||||
check-clock-reset-domains tech-cell-test rtl-lint check-rtl-lint \
|
check-clock-reset-domains tech-cell-test rtl-lint check-rtl-lint \
|
||||||
formal formal-bus formal-rib-adapter formal-rib2apb formal-gpio formal-ws2812 formal-uart formal-i2c formal-timer formal-dvp formal-i2s formal-onchip-ram formal-opipsram formal-dma formal-apu formal-apu-kws formal-gateway-a formal-sdio formal-clean formal-doctor \
|
formal formal-bus formal-rib-adapter formal-rib2apb formal-gpio formal-ws2812 formal-uart formal-i2c formal-timer formal-dvp formal-i2s formal-onchip-ram formal-opipsram formal-dma formal-apu formal-apu-kws formal-gateway-a formal-sdio formal-clean formal-doctor \
|
||||||
@@ -373,6 +390,7 @@ help:
|
|||||||
' synth-ga2d-block isolated GA2D block synthesis evidence' \
|
' synth-ga2d-block isolated GA2D block synthesis evidence' \
|
||||||
' netsim-ga2d-block GA2D synthesized-block transaction test' \
|
' netsim-ga2d-block GA2D synthesized-block transaction test' \
|
||||||
' sta-ga2d-block GA2D block timing analysis' \
|
' sta-ga2d-block GA2D block timing analysis' \
|
||||||
|
' apu-block-evidence compare P5/P7 APU block synthesis and STA' \
|
||||||
' librelane-doctor validate the IHP130 LibreLane Chip flow' \
|
' librelane-doctor validate the IHP130 LibreLane Chip flow' \
|
||||||
' librelane-chip run the single-level IHP130 pad-ring flow' \
|
' librelane-chip run the single-level IHP130 pad-ring flow' \
|
||||||
' librelane-openroad open the current Chip run in OpenROAD' \
|
' librelane-openroad open the current Chip run in OpenROAD' \
|
||||||
@@ -387,12 +405,15 @@ help:
|
|||||||
' setup-apu-kws-reference validate pinned P7 KWS model/corpus inputs' \
|
' setup-apu-kws-reference validate pinned P7 KWS model/corpus inputs' \
|
||||||
' apu-p5-bundle build the deterministic WAV/FLAC APUMC bundle' \
|
' apu-p5-bundle build the deterministic WAV/FLAC APUMC bundle' \
|
||||||
' apu-p5-corpus qualify pinned FLAC with BAM/libFLAC and production RTL' \
|
' apu-p5-corpus qualify pinned FLAC with BAM/libFLAC and production RTL' \
|
||||||
|
' apu-p7-model convert the locked MLPerf Tiny KWS model to APUM' \
|
||||||
' setup-regression install pinned dependencies for all PR PDK profiles' \
|
' setup-regression install pinned dependencies for all PR PDK profiles' \
|
||||||
' setup-hp-linux install pinned Buildroot, Linux, and OpenSBI sources' \
|
' setup-hp-linux install pinned Buildroot, Linux, and OpenSBI sources' \
|
||||||
' hp-linux build the pinned RV32 HP Linux image set' \
|
' hp-linux build the pinned RV32 HP Linux image set' \
|
||||||
' hp-bundle package LP firmware and HP Linux images for flash' \
|
' hp-bundle package LP firmware and HP Linux images for flash' \
|
||||||
' hp-linux-sim run the fast-flash HP Linux userspace acceptance test' \
|
' hp-linux-sim run the fast-flash HP Linux userspace acceptance test' \
|
||||||
' hp-smoke-sim run LP release, HP MMIO, and mailbox RTL smoke test' \
|
' hp-smoke-sim run LP release, HP MMIO, and mailbox RTL smoke test' \
|
||||||
|
' hp-apu-bundle package the APU release LP firmware and HP payload' \
|
||||||
|
' hp-apu-sim run the LP/HP APU ownership evidence simulation' \
|
||||||
' doctor check tools, paths, and selected configuration' \
|
' doctor check tools, paths, and selected configuration' \
|
||||||
' config | manifest print/write the effective configuration' \
|
' config | manifest print/write the effective configuration' \
|
||||||
' memory-map generate the selected address-map artifacts' \
|
' memory-map generate the selected address-map artifacts' \
|
||||||
@@ -449,7 +470,7 @@ config:
|
|||||||
VCS_USE_LSF '$(VCS_USE_LSF)' PDK '$(PDK)' \
|
VCS_USE_LSF '$(VCS_USE_LSF)' PDK '$(PDK)' \
|
||||||
HAVE_PLL '$(HAVE_PLL)' HAVE_SRAM_IF '$(HAVE_SRAM_IF)' \
|
HAVE_PLL '$(HAVE_PLL)' HAVE_SRAM_IF '$(HAVE_SRAM_IF)' \
|
||||||
HAVE_SRAM_MACRO '$(HAVE_SRAM_MACRO)' SRAM_SIZE_KIB '$(SRAM_SIZE_KIB)' \
|
HAVE_SRAM_MACRO '$(HAVE_SRAM_MACRO)' SRAM_SIZE_KIB '$(SRAM_SIZE_KIB)' \
|
||||||
PDK_BEHAV '$(PDK_BEHAV)' HAVE_SVA '$(HAVE_SVA)' \
|
PDK_BEHAV '$(PDK_BEHAV)' HAVE_SVA '$(HAVE_SVA)' APU_ENABLE_P7 '$(APU_ENABLE_P7)' \
|
||||||
HAVE_HP '$(HAVE_HP)' HP_CONFIG '$(HP_CONFIG)' BUILD_RELEASE '$(BUILD_RELEASE)' \
|
HAVE_HP '$(HAVE_HP)' HP_CONFIG '$(HP_CONFIG)' BUILD_RELEASE '$(BUILD_RELEASE)' \
|
||||||
JTAG_IDCODE '$(JTAG_IDCODE)' EXT_CLK_HZ '$(EXT_CLK_HZ)' AUD_CLK_HZ '$(AUD_CLK_HZ)' \
|
JTAG_IDCODE '$(JTAG_IDCODE)' EXT_CLK_HZ '$(EXT_CLK_HZ)' AUD_CLK_HZ '$(AUD_CLK_HZ)' \
|
||||||
CLINT_TIMEBASE_HZ '$(CLINT_TIMEBASE_HZ)' MGMT_CPU_CLK_HZ '$(MGMT_CPU_CLK_HZ)' \
|
CLINT_TIMEBASE_HZ '$(CLINT_TIMEBASE_HZ)' MGMT_CPU_CLK_HZ '$(MGMT_CPU_CLK_HZ)' \
|
||||||
@@ -527,6 +548,13 @@ $(APU_P5_BUNDLE): $(ROOT_PATH)/scripts/build_apu_p5_bundle.py \
|
|||||||
|
|
||||||
apu-p5-bundle: $(APU_P5_BUNDLE)
|
apu-p5-bundle: $(APU_P5_BUNDLE)
|
||||||
|
|
||||||
|
$(APU_P7_MODEL): $(ROOT_PATH)/scripts/apu_kws_convert.py \
|
||||||
|
$(ROOT_PATH)/scripts/apu_kws.py $(APU_P7_KWS_TFLITE)
|
||||||
|
python3 $(ROOT_PATH)/scripts/apu_kws_convert.py --target p7 \
|
||||||
|
--model $(APU_P7_KWS_TFLITE) --output $@ --manifest $(APU_P7_MODEL_MANIFEST)
|
||||||
|
|
||||||
|
apu-p7-model: $(APU_P7_MODEL)
|
||||||
|
|
||||||
apu-p5-corpus: setup-apu-reference $(APU_P5_BUNDLE)
|
apu-p5-corpus: setup-apu-reference $(APU_P5_BUNDLE)
|
||||||
python3 $(ROOT_PATH)/scripts/qualify_apu_p5_corpus.py \
|
python3 $(ROOT_PATH)/scripts/qualify_apu_p5_corpus.py \
|
||||||
--flac $(APU_P5_REFERENCE_DIR)/src/flac/flac \
|
--flac $(APU_P5_REFERENCE_DIR)/src/flac/flac \
|
||||||
@@ -616,6 +644,45 @@ hp-smoke-sim: hp-smoke-bundle comp
|
|||||||
--require 'HP_GA2D_PASS' \
|
--require 'HP_GA2D_PASS' \
|
||||||
--require 'HP_GA2D_CACHE_CLEAN'
|
--require 'HP_GA2D_CACHE_CLEAN'
|
||||||
|
|
||||||
|
$(HP_APU_STAMP): $(ROOT_PATH)/scripts/build_hp_apu.py \
|
||||||
|
$(ROOT_PATH)/app/ports/hp-apu/start.S \
|
||||||
|
$(ROOT_PATH)/app/ports/hp-apu/main.c \
|
||||||
|
$(ROOT_PATH)/app/ports/hp-apu/linker.ld \
|
||||||
|
$(ROOT_PATH)/app/apps/apu_release/apu_release_page.h \
|
||||||
|
$(ROOT_PATH)/crt/include/retrosoc/hal/apu_regs.h \
|
||||||
|
$(MEMORY_MAP_STAMP) $(USER_EXTENSIONS_STAMP)
|
||||||
|
python3 $(ROOT_PATH)/scripts/build_hp_apu.py \
|
||||||
|
--source-dir $(ROOT_PATH)/app/ports/hp-apu \
|
||||||
|
--output $(HP_APU_BUILD_DIR) --cross $(CROSS) \
|
||||||
|
--include $(MEMORY_MAP_C_DIR) \
|
||||||
|
--include $(USER_EXTENSIONS_DIR)/include \
|
||||||
|
--include $(ROOT_PATH)/crt/include \
|
||||||
|
--include $(ROOT_PATH)/app/apps/apu_release
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
$(HP_APU_BUNDLE_BIN): $(FIRMWARE_ELF) $(HP_APU_STAMP) \
|
||||||
|
$(ROOT_PATH)/scripts/package_hp_boot.py
|
||||||
|
python3 $(ROOT_PATH)/scripts/package_hp_boot.py \
|
||||||
|
--firmware $(SW_BUILD_DIR)/$(FIRMWARE_NAME).bin \
|
||||||
|
--images $(HP_APU_BUILD_DIR)/images --output $@ \
|
||||||
|
--manifest $(HP_APU_MANIFEST)
|
||||||
|
|
||||||
|
$(HP_APU_BUNDLE_HEX): $(HP_APU_BUNDLE_BIN)
|
||||||
|
$(OBJC) -I binary -O verilog $< $@
|
||||||
|
|
||||||
|
hp-apu-bundle: $(HP_APU_BUNDLE_BIN) $(HP_APU_BUNDLE_HEX)
|
||||||
|
|
||||||
|
hp-apu-sim: hp-apu-bundle comp
|
||||||
|
@test '$(SIMU)' = VERILATOR
|
||||||
|
$(MAKE) BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) SIM_FIRMWARE_NAME=$(HP_APU_BUNDLE_NAME) \
|
||||||
|
VERILATOR_SIM_ARGS=--fast-flash sim
|
||||||
|
python3 $(ROOT_PATH)/scripts/check_simulation.py \
|
||||||
|
--log $(SIM_BUILD_ROOT)/sim.log \
|
||||||
|
--result $(SIM_BUILD_ROOT)/result-hp-apu-sim-check.json \
|
||||||
|
--require 'VERILATOR_FAST_FLASH=enabled' \
|
||||||
|
--require 'APU_RELEASE_PASS' \
|
||||||
|
--require 'SIM_TEST_PASS code=0'
|
||||||
|
|
||||||
ifeq ($(HAVE_HP),YES)
|
ifeq ($(HAVE_HP),YES)
|
||||||
$(HP_GENERATED_STAMP): $(ROOT_PATH)/scripts/generate_vexiiriscv.py \
|
$(HP_GENERATED_STAMP): $(ROOT_PATH)/scripts/generate_vexiiriscv.py \
|
||||||
$(ROOT_PATH)/scripts/vexiiriscv/GenerateRetroSocHp.scala $(LOCK_FILE)
|
$(ROOT_PATH)/scripts/vexiiriscv/GenerateRetroSocHp.scala $(LOCK_FILE)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ The build selects an application with `APP=<name>`. The supported profiles are:
|
|||||||
| `coremark` | SRAM-resident Hazard3 CoreMark measurement; use the committed quick or standard profile. |
|
| `coremark` | SRAM-resident Hazard3 CoreMark measurement; use the committed quick or standard profile. |
|
||||||
| `debug` | Minimal SRAM image used only by the Hazard3 OpenOCD/GDB acceptance flow. |
|
| `debug` | Minimal SRAM image used only by the Hazard3 OpenOCD/GDB acceptance flow. |
|
||||||
| `hp_boot` | SRAM-resident LP loader that validates the HP Linux flash bundle, loads SDRAM, releases HP, and monitors the mailbox verdict. |
|
| `hp_boot` | SRAM-resident LP loader that validates the HP Linux flash bundle, loads SDRAM, releases HP, and monitors the mailbox verdict. |
|
||||||
|
| `apu_release` | PSRAM-resident LP/HP APU evidence flow: LP-only microcode/KWS model load, ownership handoff to HP, HP-submitted WAV/KWS jobs with Zicbom maintenance, and LP-only register fault probe. |
|
||||||
| `shell` | Interactive application that adds shell services, board drivers, media, FatFs, CoreMark, and UserIP integration. |
|
| `shell` | Interactive application that adds shell services, board drivers, media, FatFs, CoreMark, and UserIP integration. |
|
||||||
| `xpi_flash_loader` | SRAM-resident, GDB-called service image for sector-preserving JTAG programming of the qualified NSS0 NOR. |
|
| `xpi_flash_loader` | SRAM-resident, GDB-called service image for sector-preserving JTAG programming of the qualified NSS0 NOR. |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
APP_SRCS += $(ROOT_PATH)/app/apps/apu_release/main.c
|
||||||
|
APP_INC_DIRS += $(ROOT_PATH)/app/apps/apu_release $(ROOT_PATH)/app/apps/hp_boot \
|
||||||
|
$(SW_BUILD_DIR)/include
|
||||||
|
|
||||||
|
APU_RELEASE_ASSETS_H := $(SW_BUILD_DIR)/include/apu_release_assets.h
|
||||||
|
|
||||||
|
$(APU_RELEASE_ASSETS_H): $(APU_P5_BUNDLE) $(APU_P7_MODEL) \
|
||||||
|
$(ROOT_PATH)/scripts/build_apu_release_assets.py \
|
||||||
|
$(ROOT_PATH)/scripts/apu_codecs.py $(ROOT_PATH)/scripts/apu_kws.py \
|
||||||
|
$(ROOT_PATH)/scripts/apu_p5_coefficients.py
|
||||||
|
python3 $(ROOT_PATH)/scripts/build_apu_release_assets.py \
|
||||||
|
--apumc $(APU_P5_BUNDLE) --apum $(APU_P7_MODEL) \
|
||||||
|
--kws-corpus $(CACHE_ROOT)/sources/apu-kws-pcm \
|
||||||
|
--output $@
|
||||||
|
|
||||||
|
$(SW_BUILD_DIR)/firmware: $(APU_RELEASE_ASSETS_H)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#ifndef RETROSOC_APU_RELEASE_PAGE_H
|
||||||
|
#define RETROSOC_APU_RELEASE_PAGE_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* LP<->HP shared SDRAM ABI for the APU-P8 ownership evidence flow. Both the
|
||||||
|
* LP apu_release application and the HP app/ports/hp-apu payload include this
|
||||||
|
* single definition; the LP stages every buffer and publishes the job page
|
||||||
|
* before releasing the HP hart. */
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_MC_ADDRESS UINT32_C(0x3BC00000)
|
||||||
|
#define RS_APU_RELEASE_MC_CAPACITY UINT32_C(0x00010000)
|
||||||
|
#define RS_APU_RELEASE_MODEL_ADDRESS UINT32_C(0x3BC10000)
|
||||||
|
#define RS_APU_RELEASE_MODEL_CAPACITY UINT32_C(0x00008000)
|
||||||
|
#define RS_APU_RELEASE_WAV_ADDRESS UINT32_C(0x3BC20000)
|
||||||
|
#define RS_APU_RELEASE_WAV_CAPACITY UINT32_C(0x00010000)
|
||||||
|
#define RS_APU_RELEASE_PCM_ADDRESS UINT32_C(0x3BC40000)
|
||||||
|
#define RS_APU_RELEASE_PCM_CAPACITY UINT32_C(0x00010000)
|
||||||
|
#define RS_APU_RELEASE_KWS_ADDRESS UINT32_C(0x3BC60000)
|
||||||
|
#define RS_APU_RELEASE_KWS_CAPACITY UINT32_C(0x00008000)
|
||||||
|
#define RS_APU_RELEASE_PAGE_ADDRESS UINT32_C(0x3BC80000)
|
||||||
|
#define RS_APU_RELEASE_PAGE_BYTES UINT32_C(0x00001000)
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_ACL_READ_BASE RS_APU_RELEASE_MC_ADDRESS
|
||||||
|
#define RS_APU_RELEASE_ACL_READ_LIMIT \
|
||||||
|
(RS_APU_RELEASE_KWS_ADDRESS + RS_APU_RELEASE_KWS_CAPACITY - 1U)
|
||||||
|
#define RS_APU_RELEASE_ACL_WRITE_BASE RS_APU_RELEASE_PCM_ADDRESS
|
||||||
|
#define RS_APU_RELEASE_ACL_WRITE_LIMIT \
|
||||||
|
(RS_APU_RELEASE_PCM_ADDRESS + RS_APU_RELEASE_PCM_CAPACITY - 1U)
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_PAGE_MAGIC UINT32_C(0x4150384A)
|
||||||
|
#define RS_APU_RELEASE_PAGE_VERSION UINT32_C(1)
|
||||||
|
#define RS_APU_RELEASE_RESULT_MAGIC UINT32_C(0x48523031)
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_JOB_RUN UINT32_C(1)
|
||||||
|
#define RS_APU_RELEASE_EVENT_DONE UINT32_C(2)
|
||||||
|
#define RS_APU_RELEASE_MAILBOX_SEQUENCE UINT32_C(1)
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_STEP_MAILBOX UINT32_C(0x0001)
|
||||||
|
#define RS_APU_RELEASE_STEP_PAGE UINT32_C(0x0002)
|
||||||
|
#define RS_APU_RELEASE_STEP_WAV_SUBMIT UINT32_C(0x0004)
|
||||||
|
#define RS_APU_RELEASE_STEP_WAV_DONE UINT32_C(0x0008)
|
||||||
|
#define RS_APU_RELEASE_STEP_KWS_ARM UINT32_C(0x0010)
|
||||||
|
#define RS_APU_RELEASE_STEP_KWS_SUBMIT UINT32_C(0x0020)
|
||||||
|
#define RS_APU_RELEASE_STEP_KWS_DONE UINT32_C(0x0040)
|
||||||
|
#define RS_APU_RELEASE_STEP_FAULT UINT32_C(0x0080)
|
||||||
|
#define RS_APU_RELEASE_STEP_PUBLISH UINT32_C(0x0100)
|
||||||
|
#define RS_APU_RELEASE_STEP_ALL UINT32_C(0x01FF)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t version;
|
||||||
|
uint32_t flags;
|
||||||
|
uint32_t wav_address;
|
||||||
|
uint32_t wav_bytes;
|
||||||
|
uint32_t wav_crc;
|
||||||
|
uint32_t output_address;
|
||||||
|
uint32_t output_capacity;
|
||||||
|
uint32_t expected_output_crc;
|
||||||
|
uint32_t expected_output_bytes;
|
||||||
|
uint32_t expected_frames;
|
||||||
|
uint32_t wav_rate;
|
||||||
|
uint32_t wav_channels;
|
||||||
|
uint32_t wav_bits;
|
||||||
|
uint32_t kws_address;
|
||||||
|
uint32_t kws_bytes;
|
||||||
|
uint32_t kws_expected_class;
|
||||||
|
uint32_t kws_threshold;
|
||||||
|
uint32_t kws_debounce;
|
||||||
|
uint32_t acl_read_base;
|
||||||
|
uint32_t acl_read_limit;
|
||||||
|
uint32_t acl_write_base;
|
||||||
|
uint32_t acl_write_limit;
|
||||||
|
uint32_t reserved_job[9];
|
||||||
|
uint32_t result_magic;
|
||||||
|
uint32_t hp_steps;
|
||||||
|
uint32_t wav_status;
|
||||||
|
uint32_t wav_input_used;
|
||||||
|
uint32_t wav_output_bytes;
|
||||||
|
uint32_t wav_frames;
|
||||||
|
uint32_t wav_output_crc;
|
||||||
|
uint32_t wav_detail;
|
||||||
|
uint32_t kws_status;
|
||||||
|
uint32_t kws_input_used;
|
||||||
|
uint32_t kws_frames;
|
||||||
|
uint32_t kws_class_id;
|
||||||
|
uint32_t kws_score;
|
||||||
|
uint32_t kws_hit;
|
||||||
|
uint32_t kws_timestamp_lo;
|
||||||
|
uint32_t kws_timestamp_hi;
|
||||||
|
uint32_t acl_read_base_readback;
|
||||||
|
uint32_t acl_read_limit_readback;
|
||||||
|
uint32_t fault_count;
|
||||||
|
uint32_t fault_mcause;
|
||||||
|
uint32_t fault_mepc;
|
||||||
|
uint32_t fault_mtval;
|
||||||
|
uint32_t hp_error;
|
||||||
|
uint32_t reserved_result[8];
|
||||||
|
} rs_apu_release_page_t;
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_PAGE_ASSERT(member, expected) \
|
||||||
|
_Static_assert(offsetof(rs_apu_release_page_t, member) == (expected), \
|
||||||
|
"APU release page offset mismatch")
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(magic, 0x00U);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(wav_address, 0x0CU);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(kws_address, 0x38U);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(acl_read_base, 0x4CU);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(result_magic, 0x80U);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(kws_status, 0xA0U);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(fault_count, 0xC8U);
|
||||||
|
RS_APU_RELEASE_PAGE_ASSERT(hp_error, 0xD8U);
|
||||||
|
#undef RS_APU_RELEASE_PAGE_ASSERT
|
||||||
|
|
||||||
|
_Static_assert(sizeof(rs_apu_release_page_t) <= RS_APU_RELEASE_PAGE_BYTES,
|
||||||
|
"APU release page must fit one 4 KiB page");
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <retrosoc/core/soc.h>
|
||||||
|
#include <retrosoc/hal/apu.h>
|
||||||
|
#include <retrosoc/hal/dma.h>
|
||||||
|
#include <retrosoc/hal/hp_mailbox.h>
|
||||||
|
#include <retrosoc/hal/resource.h>
|
||||||
|
#include <retrosoc/hal/sdram.h>
|
||||||
|
#include <retrosoc/hal/sysctrl.h>
|
||||||
|
#include <retrosoc/hal/uart.h>
|
||||||
|
#include <retrosoc/lib/printf.h>
|
||||||
|
#include <retrosoc/service/test.h>
|
||||||
|
|
||||||
|
#include "hp_boot_bundle.h"
|
||||||
|
#include "apu_release_page.h"
|
||||||
|
#include "apu_release_assets.h"
|
||||||
|
|
||||||
|
#define RS_APU_RELEASE_DMA_TIMEOUT (RS_TIMEOUT_DEFAULT * UINT32_C(64))
|
||||||
|
#define RS_APU_RELEASE_QUIESCE_BUDGET RS_TIMEOUT_DEFAULT
|
||||||
|
/* Loader polls count APB round trips, not cycles: on the Verilator SoC each
|
||||||
|
* read crosses several clock domains, so the image fetches can consume tens of
|
||||||
|
* thousands of polls before publishing. The budget bounds only a genuinely
|
||||||
|
* stuck loader; every hardware failure path terminates the load early. */
|
||||||
|
#define RS_APU_RELEASE_LOAD_TIMEOUT UINT32_C(250000)
|
||||||
|
#define RS_APU_RELEASE_EVENT_BUDGET UINT32_C(0x04000000)
|
||||||
|
/* Settling spins between the SDRAM result flag and the single mailbox read:
|
||||||
|
* covers the tail of the HP's publish writes so the read runs on a quiet bus. */
|
||||||
|
#define RS_APU_RELEASE_SETTLE_SPINS UINT32_C(100000)
|
||||||
|
/* Bounded poll for the page result magic to become visible after the mailbox
|
||||||
|
* event: the HP CBO-cleans the page before publishing, but the writeback and
|
||||||
|
* the event cross different interconnect paths, so the page can lag the event
|
||||||
|
* by a few hundred cycles. Each poll is one SDRAM round trip. */
|
||||||
|
#define RS_APU_RELEASE_SETTLE_BUDGET UINT32_C(100000)
|
||||||
|
#define RS_APU_RELEASE_FAULT_MCAUSE UINT32_C(7)
|
||||||
|
|
||||||
|
static rs_dma_tcd_t s_apu_release_tcd __attribute__((aligned(64)));
|
||||||
|
|
||||||
|
_Static_assert(sizeof(rs_hp_boot_header_t) == UINT32_C(128), "HP bundle header ABI mismatch");
|
||||||
|
|
||||||
|
static uint32_t rs_apu_release_crc32_byte(uint32_t crc, uint8_t value) {
|
||||||
|
crc ^= value;
|
||||||
|
for (uint32_t bit = 0U; bit < 8U; ++bit) {
|
||||||
|
uint32_t mask = UINT32_C(0) - (crc & UINT32_C(1));
|
||||||
|
crc = (crc >> 1U) ^ (UINT32_C(0xEDB88320) & mask);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t rs_apu_release_crc32(const volatile uint8_t *data, uint32_t size) {
|
||||||
|
uint32_t crc = UINT32_C(0xFFFFFFFF);
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < size; ++index) {
|
||||||
|
crc = rs_apu_release_crc32_byte(crc, data[index]);
|
||||||
|
}
|
||||||
|
return ~crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_apu_release_fail(uint8_t code) {
|
||||||
|
/* Report the failure BEFORE draining the HP: the drain flush clears the
|
||||||
|
* LP's data-plane CDC, which would drop an in-flight printf stack access
|
||||||
|
* and wedge the hart before the diagnostic ever reaches the UART. */
|
||||||
|
printf("APU_RELEASE_FAIL:%u\n", (unsigned int)code);
|
||||||
|
(void)rs_sysctrl_set_hp_release(false);
|
||||||
|
rs_test_finish(RS_TEST_FAILED, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_wait_sdram(void) {
|
||||||
|
rs_sdram_status_t status;
|
||||||
|
|
||||||
|
for (uint32_t timeout = 0U; timeout < RS_TIMEOUT_DEFAULT; ++timeout) {
|
||||||
|
if ((rs_sdram_get_status(&status) == RS_OK) && status.ready && !status.init_busy &&
|
||||||
|
!status.error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_apu_release_read_header(rs_hp_boot_header_t *header) {
|
||||||
|
volatile const uint8_t *source =
|
||||||
|
(volatile const uint8_t *)(uintptr_t)(RS_SOC_FLASH_BASE + RS_HP_BOOT_BUNDLE_OFFSET);
|
||||||
|
uint8_t *destination = (uint8_t *)header;
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < sizeof(*header); ++index) {
|
||||||
|
destination[index] = source[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_range_valid(uint32_t address, uint32_t size, uint32_t base,
|
||||||
|
uint32_t capacity) {
|
||||||
|
return (size != 0U) && (address >= base) && (address < (base + capacity)) &&
|
||||||
|
((size - 1U) <= ((base + capacity - 1U) - address));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_header_valid(rs_hp_boot_header_t *header) {
|
||||||
|
uint32_t expected_crc = header->header_crc32;
|
||||||
|
|
||||||
|
header->header_crc32 = 0U;
|
||||||
|
if ((header->magic != RS_HP_BOOT_BUNDLE_MAGIC) ||
|
||||||
|
(header->version != RS_HP_BOOT_BUNDLE_VERSION) ||
|
||||||
|
(header->header_size != sizeof(*header)) ||
|
||||||
|
(header->entry_count != RS_HP_BOOT_BUNDLE_ENTRY_COUNT) ||
|
||||||
|
(header->total_size < sizeof(*header)) ||
|
||||||
|
(header->total_size > (RS_SOC_FLASH_SIZE - RS_HP_BOOT_BUNDLE_OFFSET)) ||
|
||||||
|
(rs_apu_release_crc32((volatile const uint8_t *)header, sizeof(*header)) != expected_crc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t index = 0U; index < RS_HP_BOOT_BUNDLE_ENTRY_COUNT; ++index) {
|
||||||
|
const rs_hp_boot_entry_t *entry = &header->entries[index];
|
||||||
|
|
||||||
|
if ((entry->size == 0U) || ((entry->flash_offset & UINT32_C(3)) != 0U) ||
|
||||||
|
((entry->load_address & UINT32_C(3)) != 0U) ||
|
||||||
|
!rs_apu_release_range_valid(entry->flash_offset, entry->size, RS_HP_BOOT_BUNDLE_OFFSET,
|
||||||
|
header->total_size) ||
|
||||||
|
!rs_apu_release_range_valid(entry->load_address, entry->size, RS_SOC_SDRAM_BASE,
|
||||||
|
RS_SOC_SDRAM_SIZE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_dma_copy(uint32_t source, uint32_t destination, uint32_t size,
|
||||||
|
uint32_t expected_crc) {
|
||||||
|
rs_status_t status;
|
||||||
|
|
||||||
|
s_apu_release_tcd.next_ptr = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.source = source;
|
||||||
|
s_apu_release_tcd.destination = destination;
|
||||||
|
s_apu_release_tcd.byte_count = size;
|
||||||
|
s_apu_release_tcd.source_stride = 0;
|
||||||
|
s_apu_release_tcd.destination_stride = 0;
|
||||||
|
s_apu_release_tcd.y_count = UINT16_C(1);
|
||||||
|
s_apu_release_tcd.reserved = UINT16_C(0);
|
||||||
|
s_apu_release_tcd.control = RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC | RS_DMA_TCD_DST_INC |
|
||||||
|
RS_DMA_TCD_CRC_ENABLE | RS_DMA_TCD_CRC_FINAL |
|
||||||
|
(UINT32_C(3) << RS_DMA_TCD_PRIORITY_SHIFT) |
|
||||||
|
(UINT32_C(16) << RS_DMA_TCD_BURST_SHIFT);
|
||||||
|
s_apu_release_tcd.control |= ((uint32_t)RS_DMA_KIND_MM_TO_MM << RS_DMA_TCD_KIND_SHIFT) |
|
||||||
|
((uint32_t)RS_DMA_REQUEST_SOFTWARE << RS_DMA_TCD_REQUEST_SHIFT);
|
||||||
|
s_apu_release_tcd.crc_expected = expected_crc;
|
||||||
|
s_apu_release_tcd.crc_seed = UINT32_C(0xFFFFFFFF);
|
||||||
|
s_apu_release_tcd.crc_result = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.status = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.bytes_done = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.error_status = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.reserved_tail = UINT32_C(0);
|
||||||
|
s_apu_release_tcd.reserved_tail2 = UINT32_C(0);
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
status = rs_dma_submit_tcd(RS_DMA_CHANNEL_HP, &s_apu_release_tcd, RS_APU_RELEASE_DMA_TIMEOUT);
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
return (status == RS_OK) && (s_apu_release_tcd.bytes_done == size) &&
|
||||||
|
(s_apu_release_tcd.crc_result == expected_crc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_cpu_copy(uint32_t source, uint32_t destination, uint32_t size,
|
||||||
|
uint32_t expected_crc) {
|
||||||
|
volatile const uint32_t *source_words = (volatile const uint32_t *)(uintptr_t)source;
|
||||||
|
volatile uint32_t *destination_words = (volatile uint32_t *)(uintptr_t)destination;
|
||||||
|
uint32_t word_count = size / (uint32_t)sizeof(uint32_t);
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < word_count; ++index) {
|
||||||
|
destination_words[index] = source_words[index];
|
||||||
|
}
|
||||||
|
for (uint32_t index = word_count * (uint32_t)sizeof(uint32_t); index < size; ++index) {
|
||||||
|
volatile const uint8_t *source_bytes = (volatile const uint8_t *)(uintptr_t)source;
|
||||||
|
volatile uint8_t *destination_bytes = (volatile uint8_t *)(uintptr_t)destination;
|
||||||
|
destination_bytes[index] = source_bytes[index];
|
||||||
|
}
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
return rs_apu_release_crc32((volatile const uint8_t *)(uintptr_t)destination, size) ==
|
||||||
|
expected_crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_stage(uint32_t source, uint32_t destination, uint32_t size,
|
||||||
|
uint32_t expected_crc) {
|
||||||
|
if (((source & UINT32_C(3)) != 0U) || ((destination & UINT32_C(63)) != 0U) || (size == 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* The DMA engine CRCs the transferred bytes end-to-end; the CPU fallback
|
||||||
|
* verifies with a readback CRC. The APU loader re-verifies the staged
|
||||||
|
* microcode/model payload CRCs in hardware, so no extra LP pass is made. */
|
||||||
|
if (!rs_apu_release_dma_copy(source, destination, size, expected_crc) &&
|
||||||
|
!rs_apu_release_cpu_copy(source, destination, size, expected_crc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_load_bundle(void) {
|
||||||
|
rs_hp_boot_header_t header;
|
||||||
|
|
||||||
|
rs_apu_release_read_header(&header);
|
||||||
|
if (!rs_apu_release_header_valid(&header)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t index = 0U; index < RS_HP_BOOT_BUNDLE_ENTRY_COUNT; ++index) {
|
||||||
|
const rs_hp_boot_entry_t *entry = &header.entries[index];
|
||||||
|
|
||||||
|
if (!rs_apu_release_dma_copy(RS_SOC_FLASH_BASE + entry->flash_offset, entry->load_address,
|
||||||
|
entry->size, entry->crc32) &&
|
||||||
|
!rs_apu_release_cpu_copy(RS_SOC_FLASH_BASE + entry->flash_offset, entry->load_address,
|
||||||
|
entry->size, entry->crc32)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_quiesce(void) {
|
||||||
|
if (rs_resource_set_lifecycle(RS_RESOURCE_APU, true, false) != RS_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t budget = 0U; budget < RS_APU_RELEASE_QUIESCE_BUDGET; ++budget) {
|
||||||
|
const uint32_t owner = RS_APU_REG(RS_APU_ABI_OWNER_STATUS);
|
||||||
|
const uint32_t status = RS_APU_REG(RS_APU_ABI_STATUS);
|
||||||
|
|
||||||
|
if (((owner & UINT32_C(3)) == 0U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_QUIESCE)) != 0U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_RESET)) == 0U) &&
|
||||||
|
((status & (UINT32_C(1) << RS_APU_ABI_STATUS_IDLE)) != 0U)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_stage_assets(void) {
|
||||||
|
if (!rs_apu_release_stage((uint32_t)(uintptr_t)rs_apu_release_apumc, RS_APU_RELEASE_MC_ADDRESS,
|
||||||
|
RS_APU_RELEASE_APUMC_SIZE, RS_APU_RELEASE_APUMC_CRC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!rs_apu_release_stage((uint32_t)(uintptr_t)rs_apu_release_apum,
|
||||||
|
RS_APU_RELEASE_MODEL_ADDRESS, RS_APU_RELEASE_APUM_SIZE,
|
||||||
|
RS_APU_RELEASE_APUM_CRC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!rs_apu_release_stage((uint32_t)(uintptr_t)rs_apu_release_wav, RS_APU_RELEASE_WAV_ADDRESS,
|
||||||
|
RS_APU_RELEASE_WAV_SIZE, RS_APU_RELEASE_WAV_CRC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!rs_apu_release_stage((uint32_t)(uintptr_t)rs_apu_release_kws_pcm,
|
||||||
|
RS_APU_RELEASE_KWS_ADDRESS, RS_APU_RELEASE_KWS_SIZE,
|
||||||
|
RS_APU_RELEASE_KWS_CRC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_apu_release_dump_apu(void) {
|
||||||
|
printf("APU_RELEASE_DBG:%08lx:%08lx:%08lx:%08lx:%08lx:%08lx:%08lx:%08lx\n",
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_STATUS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_OWNER_STATUS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_MC_STATUS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_MC_ACTUAL_CRC),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_KWS_MODEL_STATUS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_ERROR_STATUS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_ERROR_ADDRESS),
|
||||||
|
(unsigned long)RS_APU_REG(RS_APU_ABI_ERROR_DETAIL));
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t rs_apu_release_load_images(void) {
|
||||||
|
const rs_apu_image_t mc_image = {
|
||||||
|
.address = RS_APU_RELEASE_MC_ADDRESS,
|
||||||
|
.bytes = RS_APU_RELEASE_APUMC_SIZE,
|
||||||
|
.expected_crc = RS_APU_RELEASE_APUMC_PAYLOAD_CRC,
|
||||||
|
};
|
||||||
|
const rs_apu_image_t model_image = {
|
||||||
|
.address = RS_APU_RELEASE_MODEL_ADDRESS,
|
||||||
|
.bytes = RS_APU_RELEASE_APUM_SIZE,
|
||||||
|
.expected_crc = RS_APU_RELEASE_APUM_PAYLOAD_CRC,
|
||||||
|
};
|
||||||
|
uint32_t status;
|
||||||
|
rs_status_t load_status;
|
||||||
|
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
load_status = rs_apu_microcode_load(&mc_image, RS_APU_RELEASE_LOAD_TIMEOUT);
|
||||||
|
if (load_status != RS_OK) {
|
||||||
|
printf("APU_RELEASE_MCERR:%d\n", (int)load_status);
|
||||||
|
rs_apu_release_dump_apu();
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
status = RS_APU_REG(RS_APU_ABI_MC_STATUS);
|
||||||
|
if (((status & (UINT32_C(1) << RS_APU_ABI_MC_STATUS_VALID)) == 0U) ||
|
||||||
|
(RS_APU_REG(RS_APU_ABI_MC_LOCK) == 0U) ||
|
||||||
|
(RS_APU_REG(RS_APU_ABI_MC_ACTUAL_CRC) != RS_APU_RELEASE_APUMC_PAYLOAD_CRC)) {
|
||||||
|
rs_apu_release_dump_apu();
|
||||||
|
return 2U;
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_MC:%08lx\n", (unsigned long)RS_APU_REG(RS_APU_ABI_MC_ACTUAL_CRC));
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
load_status = rs_apu_kws_model_load(&model_image, RS_APU_RELEASE_LOAD_TIMEOUT);
|
||||||
|
if (load_status != RS_OK) {
|
||||||
|
printf("APU_RELEASE_MODELERR:%d\n", (int)load_status);
|
||||||
|
rs_apu_release_dump_apu();
|
||||||
|
return 3U;
|
||||||
|
}
|
||||||
|
status = RS_APU_REG(RS_APU_ABI_KWS_MODEL_STATUS);
|
||||||
|
if (((status & UINT32_C(6)) != UINT32_C(6)) ||
|
||||||
|
(RS_APU_REG(RS_APU_ABI_KWS_MODEL_ACTUAL_CRC) != RS_APU_RELEASE_APUM_PAYLOAD_CRC)) {
|
||||||
|
rs_apu_release_dump_apu();
|
||||||
|
return 4U;
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_MODEL:%08lx\n", (unsigned long)RS_APU_REG(RS_APU_ABI_KWS_MODEL_ACTUAL_CRC));
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_apu_release_fill_page(void) {
|
||||||
|
volatile rs_apu_release_page_t *page =
|
||||||
|
(volatile rs_apu_release_page_t *)(uintptr_t)RS_APU_RELEASE_PAGE_ADDRESS;
|
||||||
|
volatile uint32_t *words = (volatile uint32_t *)(uintptr_t)RS_APU_RELEASE_PAGE_ADDRESS;
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < (uint32_t)(sizeof(rs_apu_release_page_t) / sizeof(uint32_t));
|
||||||
|
++index) {
|
||||||
|
words[index] = UINT32_C(0);
|
||||||
|
}
|
||||||
|
page->magic = RS_APU_RELEASE_PAGE_MAGIC;
|
||||||
|
page->version = RS_APU_RELEASE_PAGE_VERSION;
|
||||||
|
page->wav_address = RS_APU_RELEASE_WAV_ADDRESS;
|
||||||
|
page->wav_bytes = RS_APU_RELEASE_WAV_SIZE;
|
||||||
|
page->wav_crc = RS_APU_RELEASE_WAV_CRC;
|
||||||
|
page->output_address = RS_APU_RELEASE_PCM_ADDRESS;
|
||||||
|
page->output_capacity = RS_APU_RELEASE_PCM_CAPACITY;
|
||||||
|
page->expected_output_crc = RS_APU_RELEASE_PCM_CRC;
|
||||||
|
page->expected_output_bytes = RS_APU_RELEASE_PCM_BYTES;
|
||||||
|
page->expected_frames = RS_APU_RELEASE_PCM_FRAMES;
|
||||||
|
page->wav_rate = RS_APU_RELEASE_WAV_RATE;
|
||||||
|
page->wav_channels = RS_APU_RELEASE_WAV_CHANNELS;
|
||||||
|
page->wav_bits = RS_APU_RELEASE_WAV_BITS;
|
||||||
|
page->kws_address = RS_APU_RELEASE_KWS_ADDRESS;
|
||||||
|
page->kws_bytes = RS_APU_RELEASE_KWS_SIZE;
|
||||||
|
page->kws_expected_class = RS_APU_RELEASE_KWS_CLASS;
|
||||||
|
page->kws_threshold = RS_APU_RELEASE_KWS_THRESHOLD;
|
||||||
|
page->kws_debounce = RS_APU_RELEASE_KWS_DEBOUNCE;
|
||||||
|
page->acl_read_base = RS_APU_RELEASE_ACL_READ_BASE;
|
||||||
|
page->acl_read_limit = RS_APU_RELEASE_ACL_READ_LIMIT;
|
||||||
|
page->acl_write_base = RS_APU_RELEASE_ACL_WRITE_BASE;
|
||||||
|
page->acl_write_limit = RS_APU_RELEASE_ACL_WRITE_LIMIT;
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_handoff(void) {
|
||||||
|
rs_resource_status_t before;
|
||||||
|
rs_resource_status_t after;
|
||||||
|
|
||||||
|
if ((rs_resource_get_status(RS_RESOURCE_APU, &before) != RS_OK) ||
|
||||||
|
(rs_resource_set_owner(RS_RESOURCE_APU, RS_RESOURCE_OWNER_HP, false) != RS_OK) ||
|
||||||
|
(rs_resource_get_status(RS_RESOURCE_APU, &after) != RS_OK)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((after.owner != RS_RESOURCE_OWNER_HP) ||
|
||||||
|
(after.handoff_count != (uint16_t)(before.handoff_count + UINT16_C(1)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t budget = 0U; budget < RS_APU_RELEASE_QUIESCE_BUDGET; ++budget) {
|
||||||
|
const uint32_t owner = RS_APU_REG(RS_APU_ABI_OWNER_STATUS);
|
||||||
|
|
||||||
|
if (((owner & UINT32_C(3)) == 1U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_QUIESCE)) == 0U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_RESET)) == 0U)) {
|
||||||
|
printf("APU_RELEASE_HANDOFF:%u\n", (unsigned int)after.handoff_count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rs_apu_release_check_page(void) {
|
||||||
|
volatile const rs_apu_release_page_t *page =
|
||||||
|
(volatile const rs_apu_release_page_t *)(uintptr_t)RS_APU_RELEASE_PAGE_ADDRESS;
|
||||||
|
const uint32_t done_bit = UINT32_C(1) << RS_APU_ABI_JOB_STATUS_DONE;
|
||||||
|
const uint32_t bad_bits = (UINT32_C(1) << RS_APU_ABI_JOB_STATUS_ERROR) |
|
||||||
|
(UINT32_C(1) << RS_APU_ABI_JOB_STATUS_ABORTED);
|
||||||
|
|
||||||
|
if ((page->result_magic != RS_APU_RELEASE_RESULT_MAGIC) ||
|
||||||
|
(page->hp_steps != RS_APU_RELEASE_STEP_ALL) || (page->hp_error != 0U)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (((page->wav_status & done_bit) == 0U) || ((page->wav_status & bad_bits) != 0U) ||
|
||||||
|
(page->wav_input_used != RS_APU_RELEASE_WAV_SIZE) ||
|
||||||
|
(page->wav_output_bytes != RS_APU_RELEASE_PCM_BYTES) ||
|
||||||
|
(page->wav_frames != RS_APU_RELEASE_PCM_FRAMES) ||
|
||||||
|
(page->wav_output_crc != RS_APU_RELEASE_PCM_CRC)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rs_apu_release_crc32((volatile const uint8_t *)(uintptr_t)RS_APU_RELEASE_PCM_ADDRESS,
|
||||||
|
RS_APU_RELEASE_PCM_BYTES) != RS_APU_RELEASE_PCM_CRC) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (((page->kws_status & done_bit) == 0U) || ((page->kws_status & bad_bits) != 0U) ||
|
||||||
|
(page->kws_input_used != UINT32_C(32000)) || (page->kws_frames != UINT32_C(16000)) ||
|
||||||
|
(page->kws_class_id != RS_APU_RELEASE_KWS_CLASS) || (page->kws_hit != 1U) ||
|
||||||
|
(page->kws_score < RS_APU_RELEASE_KWS_THRESHOLD)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((page->acl_read_base_readback != RS_APU_RELEASE_ACL_READ_BASE) ||
|
||||||
|
(page->acl_read_limit_readback != RS_APU_RELEASE_ACL_READ_LIMIT)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((page->fault_count == 0U) || (page->fault_mcause != RS_APU_RELEASE_FAULT_MCAUSE) ||
|
||||||
|
(page->fault_mtval != (RS_SOC_APB4_APU_BASE + RS_APU_ABI_READ_BASE))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_apu_release_run(void) {
|
||||||
|
rs_apu_info_t info;
|
||||||
|
rs_sysctrl_hp_status_t hp_status;
|
||||||
|
rs_hp_mailbox_message_t event;
|
||||||
|
volatile rs_apu_release_page_t *page =
|
||||||
|
(volatile rs_apu_release_page_t *)(uintptr_t)RS_APU_RELEASE_PAGE_ADDRESS;
|
||||||
|
uint32_t budget;
|
||||||
|
|
||||||
|
if (rs_uart_init(CPU_FREQ * UINT32_C(1000000), UART_BPS) != RS_OK) {
|
||||||
|
rs_test_finish(RS_TEST_FAILED, UINT8_C(1));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_BOOT\n");
|
||||||
|
if ((rs_sysctrl_set_hp_release(false) != RS_OK) ||
|
||||||
|
(rs_sysctrl_select_hp_debug(true) != RS_OK) ||
|
||||||
|
(rs_sysctrl_get_hp_status(&hp_status) != RS_OK) || !hp_status.present ||
|
||||||
|
!hp_status.reset_asserted) {
|
||||||
|
rs_apu_release_fail(UINT8_C(2));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_HP_HELD\n");
|
||||||
|
if (!rs_apu_release_wait_sdram()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(3));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_SDRAM\n");
|
||||||
|
if (!rs_apu_release_load_bundle()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(4));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_BUNDLE\n");
|
||||||
|
if ((rs_apu_probe(&info) != RS_OK) || (info.capability0 != RS_APU_CAPABILITY0_P7_IMPLEMENTED) ||
|
||||||
|
(info.abi_digest != RS_APU_DIGEST_P7_IMPLEMENTED)) {
|
||||||
|
rs_apu_release_fail(UINT8_C(5));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_PROBE:%08lx:%08lx\n", (unsigned long)info.capability0,
|
||||||
|
(unsigned long)info.capability1);
|
||||||
|
if (!rs_apu_release_quiesce()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(6));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_QUIESCED\n");
|
||||||
|
if (!rs_apu_release_stage_assets()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(7));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_STAGED\n");
|
||||||
|
if (rs_apu_set_acl(RS_APU_RELEASE_ACL_READ_BASE, RS_APU_RELEASE_ACL_READ_LIMIT,
|
||||||
|
RS_APU_RELEASE_ACL_WRITE_BASE, RS_APU_RELEASE_ACL_WRITE_LIMIT) != RS_OK) {
|
||||||
|
rs_apu_release_fail(UINT8_C(8));
|
||||||
|
}
|
||||||
|
if (rs_apu_release_load_images() != 0U) {
|
||||||
|
rs_apu_release_fail(UINT8_C(9));
|
||||||
|
}
|
||||||
|
if (!rs_apu_release_handoff()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(10));
|
||||||
|
}
|
||||||
|
rs_apu_release_fill_page();
|
||||||
|
(void)rs_hp_mailbox_clear_lp_interrupt();
|
||||||
|
if (rs_hp_mailbox_send_to_hp(&(const rs_hp_mailbox_message_t){
|
||||||
|
.code = RS_APU_RELEASE_JOB_RUN,
|
||||||
|
.argument = RS_APU_RELEASE_PAGE_ADDRESS,
|
||||||
|
.sequence = RS_APU_RELEASE_MAILBOX_SEQUENCE,
|
||||||
|
}) != RS_OK) {
|
||||||
|
rs_apu_release_fail(UINT8_C(11));
|
||||||
|
}
|
||||||
|
if ((rs_sysctrl_set_hp_release(true) != RS_OK) ||
|
||||||
|
(rs_sysctrl_get_hp_status(&hp_status) != RS_OK) || !hp_status.released) {
|
||||||
|
rs_apu_release_fail(UINT8_C(12));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_HP_RUN\n");
|
||||||
|
|
||||||
|
/* Wait for the HP done event; the bounded poll only exits early on a stuck
|
||||||
|
* hart because every hardware failure path publishes its own event. */
|
||||||
|
event.code = 0U;
|
||||||
|
event.argument = 0U;
|
||||||
|
event.sequence = 0U;
|
||||||
|
for (budget = 0U; (budget < RS_APU_RELEASE_EVENT_BUDGET) && (event.sequence == 0U); ++budget) {
|
||||||
|
(void)rs_hp_mailbox_receive_from_hp(&event);
|
||||||
|
}
|
||||||
|
if ((event.sequence == 0U) || (event.code != RS_APU_RELEASE_EVENT_DONE) ||
|
||||||
|
(event.argument != 0U)) {
|
||||||
|
rs_apu_release_fail(UINT8_C(13));
|
||||||
|
}
|
||||||
|
for (budget = 0U; budget < RS_APU_RELEASE_SETTLE_SPINS; ++budget) {
|
||||||
|
__asm__ volatile("nop" ::: "memory");
|
||||||
|
}
|
||||||
|
for (budget = 0U; (budget < RS_APU_RELEASE_SETTLE_BUDGET) &&
|
||||||
|
(page->result_magic != RS_APU_RELEASE_RESULT_MAGIC);
|
||||||
|
++budget) {
|
||||||
|
}
|
||||||
|
if (!rs_apu_release_check_page()) {
|
||||||
|
rs_apu_release_fail(UINT8_C(14));
|
||||||
|
}
|
||||||
|
printf("APU_RELEASE_WAV:%lu:%lu\n", (unsigned long)page->wav_frames,
|
||||||
|
(unsigned long)page->wav_output_crc);
|
||||||
|
printf("APU_RELEASE_KWS:%lu:%lu\n", (unsigned long)page->kws_class_id,
|
||||||
|
(unsigned long)page->kws_score);
|
||||||
|
printf("APU_RELEASE_TRAP:%lu:%lu\n", (unsigned long)page->fault_mcause,
|
||||||
|
(unsigned long)page->fault_mtval);
|
||||||
|
printf("APU_RELEASE_PASS\n");
|
||||||
|
rs_test_finish(RS_TEST_PASSED, UINT8_C(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
rs_apu_release_run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
ENTRY(_start)
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
. = 0x38000000;
|
||||||
|
.text :
|
||||||
|
{
|
||||||
|
KEEP(*(.text.start))
|
||||||
|
*(.text*)
|
||||||
|
*(.rodata*)
|
||||||
|
*(.srodata*)
|
||||||
|
}
|
||||||
|
. = ALIGN(8);
|
||||||
|
.data :
|
||||||
|
{
|
||||||
|
*(.data*)
|
||||||
|
*(.sdata*)
|
||||||
|
}
|
||||||
|
. = ALIGN(8);
|
||||||
|
__bss_start = .;
|
||||||
|
.bss :
|
||||||
|
{
|
||||||
|
*(.sbss*)
|
||||||
|
*(.bss*)
|
||||||
|
*(COMMON)
|
||||||
|
}
|
||||||
|
. = ALIGN(8);
|
||||||
|
__bss_end = .;
|
||||||
|
. = ALIGN(16);
|
||||||
|
.stack :
|
||||||
|
{
|
||||||
|
. += 0x2000;
|
||||||
|
__stack_top = .;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <retrosoc/hal/apu_regs.h>
|
||||||
|
|
||||||
|
#include "apu_release_page.h"
|
||||||
|
|
||||||
|
#define HP_UART_BAUD_INT UINT32_C(0x00)
|
||||||
|
#define HP_UART_BAUD_FRAC UINT32_C(0x04)
|
||||||
|
#define HP_UART_LINE_CTRL UINT32_C(0x08)
|
||||||
|
#define HP_UART_CTRL UINT32_C(0x0C)
|
||||||
|
#define HP_UART_TXDATA UINT32_C(0x10)
|
||||||
|
#define HP_UART_STATUS UINT32_C(0x18)
|
||||||
|
#define HP_UART_FIFO_CTRL UINT32_C(0x20)
|
||||||
|
#define HP_UART_ERROR UINT32_C(0x30)
|
||||||
|
#define HP_UART_INTR_ENABLE UINT32_C(0x38)
|
||||||
|
#define HP_UART_TX_FULL UINT32_C(0x20)
|
||||||
|
|
||||||
|
#define HP_MAILBOX_LP_COMMAND UINT32_C(0x10)
|
||||||
|
#define HP_MAILBOX_LP_ARG0 UINT32_C(0x14)
|
||||||
|
#define HP_MAILBOX_LP_SEQUENCE UINT32_C(0x18)
|
||||||
|
#define HP_MAILBOX_HP_EVENT UINT32_C(0x20)
|
||||||
|
#define HP_MAILBOX_HP_ARG0 UINT32_C(0x24)
|
||||||
|
#define HP_MAILBOX_HP_SEQUENCE UINT32_C(0x28)
|
||||||
|
#define HP_MAILBOX_HP_DOORBELL UINT32_C(0x2C)
|
||||||
|
#define HP_MAILBOX_HP_INTR_STATE UINT32_C(0x40)
|
||||||
|
|
||||||
|
#define HP_CBO_LINE_BYTES UINT32_C(64)
|
||||||
|
#define HP_MAILBOX_BUDGET UINT32_C(0x08000000)
|
||||||
|
#define HP_JOB_BUDGET UINT32_C(0x08000000)
|
||||||
|
#define HP_JOB_TERMINAL \
|
||||||
|
((UINT32_C(1) << RS_APU_ABI_JOB_STATUS_DONE) | (UINT32_C(1) << RS_APU_ABI_JOB_STATUS_ERROR) | \
|
||||||
|
(UINT32_C(1) << RS_APU_ABI_JOB_STATUS_ABORTED))
|
||||||
|
|
||||||
|
volatile uint32_t hp_fault_record[5];
|
||||||
|
|
||||||
|
static volatile uint32_t *hp_reg(uint32_t address) {
|
||||||
|
return (volatile uint32_t *)(uintptr_t)address;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_uart_init(void) {
|
||||||
|
volatile uint32_t *const uart = hp_reg(RS_SOC_APB4_UART1_BASE);
|
||||||
|
|
||||||
|
uart[HP_UART_CTRL / 4U] = 0U;
|
||||||
|
uart[HP_UART_FIFO_CTRL / 4U] = 3U;
|
||||||
|
uart[HP_UART_ERROR / 4U] = 0x7FU;
|
||||||
|
uart[HP_UART_INTR_ENABLE / 4U] = 0U;
|
||||||
|
uart[HP_UART_BAUD_INT / 4U] = 78U;
|
||||||
|
uart[HP_UART_BAUD_FRAC / 4U] = 32U;
|
||||||
|
uart[HP_UART_LINE_CTRL / 4U] = 3U;
|
||||||
|
uart[HP_UART_CTRL / 4U] = 3U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_uart_putc(char value) {
|
||||||
|
volatile uint32_t *const uart = hp_reg(RS_SOC_APB4_UART1_BASE);
|
||||||
|
|
||||||
|
while ((uart[HP_UART_STATUS / 4U] & HP_UART_TX_FULL) != 0U) {
|
||||||
|
}
|
||||||
|
uart[HP_UART_TXDATA / 4U] = (uint32_t)(uint8_t)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_uart_puts(const char *text) {
|
||||||
|
for (uint32_t index = 0U; text[index] != '\0'; ++index) {
|
||||||
|
hp_uart_putc(text[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_uart_puthex(uint32_t value) {
|
||||||
|
char digits[8];
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < 8U; ++index) {
|
||||||
|
const uint32_t nibble = (value >> ((7U - index) * 4U)) & UINT32_C(0xF);
|
||||||
|
digits[index] = (char)((nibble < 10U) ? ('0' + nibble) : ('A' + (nibble - 10U)));
|
||||||
|
}
|
||||||
|
for (uint32_t index = 0U; index < 8U; ++index) {
|
||||||
|
hp_uart_putc(digits[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_cbo_clean(uint32_t address, uint32_t bytes) {
|
||||||
|
uint32_t cursor = address & ~(HP_CBO_LINE_BYTES - 1U);
|
||||||
|
const uint32_t end = address + bytes;
|
||||||
|
|
||||||
|
while (cursor < end) {
|
||||||
|
__asm__ volatile("cbo.clean 0(%0)" ::"r"(cursor) : "memory");
|
||||||
|
cursor += HP_CBO_LINE_BYTES;
|
||||||
|
}
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_cbo_inval(uint32_t address, uint32_t bytes) {
|
||||||
|
uint32_t cursor = address & ~(HP_CBO_LINE_BYTES - 1U);
|
||||||
|
const uint32_t end = address + bytes;
|
||||||
|
|
||||||
|
while (cursor < end) {
|
||||||
|
__asm__ volatile("cbo.inval 0(%0)" ::"r"(cursor) : "memory");
|
||||||
|
cursor += HP_CBO_LINE_BYTES;
|
||||||
|
}
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t hp_crc32_byte(uint32_t crc, uint8_t value) {
|
||||||
|
crc ^= value;
|
||||||
|
for (uint32_t bit = 0U; bit < 8U; ++bit) {
|
||||||
|
const uint32_t mask = UINT32_C(0) - (crc & UINT32_C(1));
|
||||||
|
crc = (crc >> 1U) ^ (UINT32_C(0xEDB88320) & mask);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t hp_crc32(const volatile uint8_t *data, uint32_t size) {
|
||||||
|
uint32_t crc = UINT32_C(0xFFFFFFFF);
|
||||||
|
|
||||||
|
for (uint32_t index = 0U; index < size; ++index) {
|
||||||
|
crc = hp_crc32_byte(crc, data[index]);
|
||||||
|
}
|
||||||
|
return ~crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_range_in(uint32_t address, uint32_t bytes, uint32_t base, uint32_t limit) {
|
||||||
|
const uint64_t last = (uint64_t)address + (uint64_t)bytes - 1U;
|
||||||
|
|
||||||
|
return (bytes != 0U) && (address >= base) && (last <= (uint64_t)limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_owner_ready(void) {
|
||||||
|
const uint32_t owner = *hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_OWNER_STATUS);
|
||||||
|
|
||||||
|
return ((owner & UINT32_C(3)) == 1U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_QUIESCE)) == 0U) &&
|
||||||
|
((owner & (UINT32_C(1) << RS_APU_ABI_OWNER_STATUS_RESET)) == 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_microcode_ready(void) {
|
||||||
|
return hp_owner_ready() &&
|
||||||
|
((*hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_MC_STATUS) &
|
||||||
|
(UINT32_C(1) << RS_APU_ABI_MC_STATUS_VALID)) != 0U) &&
|
||||||
|
(*hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_MC_LOCK) != 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_wait_microcode(void) {
|
||||||
|
for (uint32_t budget = 0U; budget < HP_JOB_BUDGET; ++budget) {
|
||||||
|
if (hp_microcode_ready()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_wait_kws_model(void) {
|
||||||
|
for (uint32_t budget = 0U; budget < HP_JOB_BUDGET; ++budget) {
|
||||||
|
if (hp_owner_ready() &&
|
||||||
|
((*hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_KWS_MODEL_STATUS) & UINT32_C(6)) ==
|
||||||
|
UINT32_C(6)) &&
|
||||||
|
(*hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_KWS_CONTROL) == 0U)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t hp_job_wait(void) {
|
||||||
|
for (uint32_t budget = 0U; budget < HP_JOB_BUDGET; ++budget) {
|
||||||
|
const uint32_t status = *hp_reg(RS_SOC_APB4_APU_BASE + RS_APU_ABI_JOB_STATUS);
|
||||||
|
|
||||||
|
if ((status & HP_JOB_TERMINAL) != 0U) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_mailbox_wait(uint32_t *code, uint32_t *argument) {
|
||||||
|
volatile uint32_t *const mailbox = hp_reg(RS_SOC_APB4_HP_MAILBOX_BASE);
|
||||||
|
|
||||||
|
for (uint32_t budget = 0U; budget < HP_MAILBOX_BUDGET; ++budget) {
|
||||||
|
if (mailbox[HP_MAILBOX_LP_SEQUENCE / 4U] != 0U) {
|
||||||
|
__asm__ volatile("fence r, r" ::: "memory");
|
||||||
|
*code = mailbox[HP_MAILBOX_LP_COMMAND / 4U];
|
||||||
|
*argument = mailbox[HP_MAILBOX_LP_ARG0 / 4U];
|
||||||
|
mailbox[HP_MAILBOX_HP_INTR_STATE / 4U] = 1U;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_submit_wav(volatile rs_apu_release_page_t *page) {
|
||||||
|
const uint32_t apu = RS_SOC_APB4_APU_BASE;
|
||||||
|
const uint32_t read_base = *hp_reg(apu + RS_APU_ABI_READ_BASE);
|
||||||
|
const uint32_t read_limit = *hp_reg(apu + RS_APU_ABI_READ_LIMIT);
|
||||||
|
const uint32_t write_base = *hp_reg(apu + RS_APU_ABI_WRITE_BASE);
|
||||||
|
const uint32_t write_limit = *hp_reg(apu + RS_APU_ABI_WRITE_LIMIT);
|
||||||
|
|
||||||
|
if (!hp_wait_microcode() ||
|
||||||
|
((*hp_reg(apu + RS_APU_ABI_JOB_STATUS) & (UINT32_C(1) << RS_APU_ABI_JOB_STATUS_BUSY)) !=
|
||||||
|
0U) ||
|
||||||
|
(*hp_reg(apu + RS_APU_ABI_RING_CONTROL) != 0U) ||
|
||||||
|
!hp_range_in(page->wav_address, page->wav_bytes, read_base, read_limit) ||
|
||||||
|
!hp_range_in(page->output_address, page->output_capacity, write_base, write_limit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hp_cbo_clean(page->wav_address, page->wav_bytes);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_CONTROL) = 0U;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_ADDRESS) = page->wav_address;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_LENGTH) = page->wav_bytes;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_ADDRESS) = page->output_address;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_CAPACITY) = page->output_capacity;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_CONFIG) =
|
||||||
|
page->wav_rate | (page->wav_channels << 17U) | (page->wav_bits << 20U);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_CONFIG) = page->wav_rate | (page->wav_channels << 17U);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_FLAGS) = 1U;
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_WAV_SUBMIT;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_COMMAND) = UINT32_C(1) << RS_APU_ABI_COMMAND_START_DIRECT;
|
||||||
|
page->wav_status = hp_job_wait();
|
||||||
|
page->wav_input_used = *hp_reg(apu + RS_APU_ABI_JOB_INPUT_USED);
|
||||||
|
page->wav_output_bytes = *hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_BYTES);
|
||||||
|
page->wav_frames = *hp_reg(apu + RS_APU_ABI_JOB_FRAMES);
|
||||||
|
page->wav_detail = *hp_reg(apu + RS_APU_ABI_JOB_DETAIL);
|
||||||
|
if ((page->wav_status & (UINT32_C(1) << RS_APU_ABI_JOB_STATUS_DONE)) == 0U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hp_cbo_inval(page->output_address, page->wav_output_bytes);
|
||||||
|
page->wav_output_crc =
|
||||||
|
hp_crc32((volatile const uint8_t *)(uintptr_t)page->output_address, page->wav_output_bytes);
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_WAV_DONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hp_submit_kws(volatile rs_apu_release_page_t *page) {
|
||||||
|
const uint32_t apu = RS_SOC_APB4_APU_BASE;
|
||||||
|
const uint32_t read_base = *hp_reg(apu + RS_APU_ABI_READ_BASE);
|
||||||
|
const uint32_t read_limit = *hp_reg(apu + RS_APU_ABI_READ_LIMIT);
|
||||||
|
uint32_t kws_result;
|
||||||
|
|
||||||
|
if (!hp_wait_kws_model() ||
|
||||||
|
!hp_range_in(page->kws_address, page->kws_bytes, read_base, read_limit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*hp_reg(apu + RS_APU_ABI_KWS_CONTROL) = UINT32_C(3);
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_KWS_ARM;
|
||||||
|
hp_cbo_clean(page->kws_address, page->kws_bytes);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_CONTROL) = UINT32_C(1);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_ADDRESS) = page->kws_address;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_LENGTH) = page->kws_bytes;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_ADDRESS) = 0U;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_CAPACITY) = 0U;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_INPUT_CONFIG) =
|
||||||
|
UINT32_C(16000) | (UINT32_C(1) << 17U) | (UINT32_C(16) << 20U);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_OUTPUT_CONFIG) = 0U;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_JOB_FLAGS) = 0U;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_KWS_CONFIG) = page->kws_threshold | (page->kws_debounce << 8U);
|
||||||
|
__asm__ volatile("fence rw, rw" ::: "memory");
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_KWS_SUBMIT;
|
||||||
|
*hp_reg(apu + RS_APU_ABI_COMMAND) = UINT32_C(1) << RS_APU_ABI_COMMAND_START_DIRECT;
|
||||||
|
page->kws_status = hp_job_wait();
|
||||||
|
page->kws_input_used = *hp_reg(apu + RS_APU_ABI_JOB_INPUT_USED);
|
||||||
|
page->kws_frames = *hp_reg(apu + RS_APU_ABI_JOB_FRAMES);
|
||||||
|
if ((page->kws_status & (UINT32_C(1) << RS_APU_ABI_JOB_STATUS_DONE)) == 0U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
kws_result = *hp_reg(apu + RS_APU_ABI_KWS_RESULT);
|
||||||
|
page->kws_timestamp_lo = *hp_reg(apu + RS_APU_ABI_KWS_TIMESTAMP_LO);
|
||||||
|
page->kws_timestamp_hi = *hp_reg(apu + RS_APU_ABI_KWS_TIMESTAMP_HI);
|
||||||
|
page->kws_class_id = kws_result & UINT32_C(0xFF);
|
||||||
|
page->kws_score = (kws_result >> 8U) & UINT32_C(0xFF);
|
||||||
|
page->kws_hit = (kws_result >> 16U) & UINT32_C(1);
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_KWS_DONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __attribute__((noinline)) hp_probe_lp_only(volatile rs_apu_release_page_t *page) {
|
||||||
|
const uint32_t apu = RS_SOC_APB4_APU_BASE;
|
||||||
|
const uint32_t faults_before = hp_fault_record[3];
|
||||||
|
|
||||||
|
page->acl_read_base_readback = *hp_reg(apu + RS_APU_ABI_READ_BASE);
|
||||||
|
page->acl_read_limit_readback = *hp_reg(apu + RS_APU_ABI_READ_LIMIT);
|
||||||
|
*hp_reg(apu + RS_APU_ABI_READ_BASE) = 0U;
|
||||||
|
/* The trap handler redirects mepc here; whether the store-fault trap is
|
||||||
|
* precise or imprecise, execution resumes at this pad exactly once and the
|
||||||
|
* mailbox publish path stays intact. */
|
||||||
|
__asm__ volatile(".globl hp_probe_recover\nhp_probe_recover:" ::: "memory");
|
||||||
|
if (hp_fault_record[3] != faults_before) {
|
||||||
|
page->fault_count = hp_fault_record[3];
|
||||||
|
page->fault_mcause = hp_fault_record[0];
|
||||||
|
page->fault_mepc = hp_fault_record[1];
|
||||||
|
page->fault_mtval = hp_fault_record[2];
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_publish(volatile rs_apu_release_page_t *page, uint32_t summary) {
|
||||||
|
volatile uint32_t *const mailbox = hp_reg(RS_SOC_APB4_HP_MAILBOX_BASE);
|
||||||
|
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_PUBLISH;
|
||||||
|
page->result_magic = RS_APU_RELEASE_RESULT_MAGIC;
|
||||||
|
hp_cbo_clean(RS_APU_RELEASE_PAGE_ADDRESS, RS_APU_RELEASE_PAGE_BYTES);
|
||||||
|
mailbox[HP_MAILBOX_HP_EVENT / 4U] = RS_APU_RELEASE_EVENT_DONE;
|
||||||
|
mailbox[HP_MAILBOX_HP_ARG0 / 4U] = summary;
|
||||||
|
mailbox[HP_MAILBOX_HP_SEQUENCE / 4U] = RS_APU_RELEASE_MAILBOX_SEQUENCE;
|
||||||
|
__asm__ volatile("fence w, w" ::: "memory");
|
||||||
|
mailbox[HP_MAILBOX_HP_DOORBELL / 4U] = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hp_report(const char *marker, uint32_t value) {
|
||||||
|
hp_uart_puts(marker);
|
||||||
|
hp_uart_puthex(value);
|
||||||
|
hp_uart_puts("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
volatile rs_apu_release_page_t *page =
|
||||||
|
(volatile rs_apu_release_page_t *)(uintptr_t)RS_APU_RELEASE_PAGE_ADDRESS;
|
||||||
|
uint32_t code = 0U;
|
||||||
|
uint32_t argument = 0U;
|
||||||
|
uint32_t summary = 1U;
|
||||||
|
|
||||||
|
hp_uart_init();
|
||||||
|
hp_uart_puts("HP_APU_BOOT\n");
|
||||||
|
if (!hp_mailbox_wait(&code, &argument)) {
|
||||||
|
hp_uart_puts("HP_APU_NO_MAILBOX\n");
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hp_cbo_inval(RS_APU_RELEASE_PAGE_ADDRESS, RS_APU_RELEASE_PAGE_BYTES);
|
||||||
|
page->hp_steps = RS_APU_RELEASE_STEP_MAILBOX;
|
||||||
|
if ((code != RS_APU_RELEASE_JOB_RUN) || (argument != RS_APU_RELEASE_PAGE_ADDRESS) ||
|
||||||
|
(page->magic != RS_APU_RELEASE_PAGE_MAGIC) ||
|
||||||
|
(page->version != RS_APU_RELEASE_PAGE_VERSION)) {
|
||||||
|
hp_report("HP_APU_BAD_PAGE", code);
|
||||||
|
page->hp_error = 2U;
|
||||||
|
hp_publish(page, 2U);
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page->hp_steps |= RS_APU_RELEASE_STEP_PAGE;
|
||||||
|
if (!hp_submit_wav(page)) {
|
||||||
|
hp_report("HP_APU_WAV_STOP", page->wav_status);
|
||||||
|
page->hp_error = 3U;
|
||||||
|
hp_publish(page, 3U);
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hp_report("HP_APU_WAV_DONE", page->wav_output_bytes);
|
||||||
|
if (!hp_submit_kws(page)) {
|
||||||
|
hp_report("HP_APU_KWS_STOP", page->kws_status);
|
||||||
|
page->hp_error = 4U;
|
||||||
|
hp_publish(page, 4U);
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hp_report("HP_APU_KWS_DONE", page->kws_class_id);
|
||||||
|
hp_probe_lp_only(page);
|
||||||
|
hp_report("HP_APU_TRAP_SEEN", page->fault_mcause);
|
||||||
|
summary = (page->hp_steps == RS_APU_RELEASE_STEP_ALL) ? 0U : 5U;
|
||||||
|
page->hp_error = summary;
|
||||||
|
hp_publish(page, summary);
|
||||||
|
hp_uart_puts("HP_APU_DONE\n");
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.section .text.start
|
||||||
|
.global _start
|
||||||
|
.type _start, @function
|
||||||
|
|
||||||
|
_start:
|
||||||
|
la sp, __stack_top
|
||||||
|
la t0, hp_trap_handler
|
||||||
|
csrw mtvec, t0
|
||||||
|
la t0, __bss_start
|
||||||
|
la t1, __bss_end
|
||||||
|
1:
|
||||||
|
bgeu t0, t1, 2f
|
||||||
|
sw zero, 0(t0)
|
||||||
|
addi t0, t0, 4
|
||||||
|
j 1b
|
||||||
|
2:
|
||||||
|
call main
|
||||||
|
3:
|
||||||
|
j 3b
|
||||||
|
|
||||||
|
.align 2
|
||||||
|
.global hp_trap_handler
|
||||||
|
.type hp_trap_handler, @function
|
||||||
|
|
||||||
|
/* Minimal M-mode trap handler for the LP-only-register store probe. Records
|
||||||
|
* mcause/mepc/mtval and bumps the observation count in hp_fault_record, then
|
||||||
|
* redirects mepc to the hp_probe_recover landing pad instead of skipping the
|
||||||
|
* faulting instruction: an imprecise store-fault mepc must not corrupt an
|
||||||
|
* innocent later instruction. */
|
||||||
|
hp_trap_handler:
|
||||||
|
csrw mscratch, t0
|
||||||
|
la t0, hp_fault_record
|
||||||
|
sw t1, 16(t0)
|
||||||
|
csrr t1, mcause
|
||||||
|
sw t1, 0(t0)
|
||||||
|
csrr t1, mepc
|
||||||
|
sw t1, 4(t0)
|
||||||
|
csrr t1, mtval
|
||||||
|
sw t1, 8(t0)
|
||||||
|
lw t1, 12(t0)
|
||||||
|
addi t1, t1, 1
|
||||||
|
sw t1, 12(t0)
|
||||||
|
la t1, hp_probe_recover
|
||||||
|
csrw mepc, t1
|
||||||
|
lw t1, 16(t0)
|
||||||
|
csrr t0, mscratch
|
||||||
|
mret
|
||||||
@@ -18,6 +18,11 @@ from 32 KiB on-chip SRAM, enables VexiiRiscv `Zicbom` with 64-byte blocks, and
|
|||||||
generates VexiiRiscv RTL only below the selected build variant. It is not part
|
generates VexiiRiscv RTL only below the selected build variant. It is not part
|
||||||
of the supported PR matrix until Linux boot, HP performance, synthesis, and
|
of the supported PR matrix until Linux boot, HP performance, synthesis, and
|
||||||
timing evidence are qualified.
|
timing evidence are qualified.
|
||||||
|
`ci/ihp130-apu.mk` is the APU LP/HP evidence profile. It runs `apu_release`
|
||||||
|
from PSRAM, stages the embedded APUMC/APUM/WAV/KWS assets through SDRAM,
|
||||||
|
performs the LP-only image loads, hands APU ownership to the HP hart, and
|
||||||
|
checks HP-submitted WAV/KWS jobs plus the LP-only register fault probe in the
|
||||||
|
full-SoC Verilator simulation via `hp-apu-sim`.
|
||||||
`ci/ihp130-xpi-flash-loader.mk` builds the SRAM-only XPI NOR service image used
|
`ci/ihp130-xpi-flash-loader.mk` builds the SRAM-only XPI NOR service image used
|
||||||
by GDB/OpenOCD; it is a programming utility, not a normal boot application.
|
by GDB/OpenOCD; it is a programming utility, not a normal boot application.
|
||||||
Start builds from a committed profile rather than setting an unreviewed mix of
|
Start builds from a committed profile rather than setting an unreviewed mix of
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
SOC := MINI
|
||||||
|
MINI_MODE := PRODUCT
|
||||||
|
PDK := IHP130
|
||||||
|
HAVE_PLL := NO
|
||||||
|
HAVE_HP := YES
|
||||||
|
APU_ENABLE_P7 := YES
|
||||||
|
HP_CONFIG := rv32imafdc_zicbom_max
|
||||||
|
HAVE_SRAM_IF := YES
|
||||||
|
HAVE_SRAM_MACRO := YES
|
||||||
|
SRAM_SIZE_KIB := 32
|
||||||
|
PDK_BEHAV := YES
|
||||||
|
EXT_CLK_HZ := 72000000
|
||||||
|
AUD_CLK_HZ := 18432000
|
||||||
|
CLINT_TIMEBASE_HZ := 1000000
|
||||||
|
ISA := RV32IM
|
||||||
|
HAVE_CSR := YES
|
||||||
|
APP := apu_release
|
||||||
|
LINK_TYPE := ld2_psram
|
||||||
|
SIM_SUCCESS_MARKER := SIM_TEST_PASS
|
||||||
|
RTL_SIM_TIMEOUT := 20000000
|
||||||
|
SOC_SIM_TIME := 3600
|
||||||
|
FIRMWARE_NAME := retrosoc_apu_release
|
||||||
|
SIM_FIRMWARE_NAME := retrosoc_apu_release
|
||||||
@@ -481,18 +481,25 @@
|
|||||||
#define RS_APU_IP_ID_VALUE UINT32_C(0x41505530)
|
#define RS_APU_IP_ID_VALUE UINT32_C(0x41505530)
|
||||||
#define RS_APU_IP_VERSION_VALUE UINT32_C(0x00010001)
|
#define RS_APU_IP_VERSION_VALUE UINT32_C(0x00010001)
|
||||||
#define RS_APU_IP_VERSION_MAJOR_MASK UINT32_C(0xFFFF0000)
|
#define RS_APU_IP_VERSION_MAJOR_MASK UINT32_C(0xFFFF0000)
|
||||||
#define RS_APU_CAPABILITY0_IMPLEMENTED UINT32_C(0x000001BD)
|
|
||||||
#define RS_APU_CAPABILITY0_P7_IMPLEMENTED UINT32_C(0x000001FD)
|
#define RS_APU_CAPABILITY0_P7_IMPLEMENTED UINT32_C(0x000001FD)
|
||||||
|
#define RS_APU_CAPABILITY0_P5_IMPLEMENTED UINT32_C(0x000001BD)
|
||||||
#define RS_APU_CAPABILITY1_P7_IMPLEMENTED UINT32_C(0x01827020)
|
#define RS_APU_CAPABILITY1_P7_IMPLEMENTED UINT32_C(0x01827020)
|
||||||
#define RS_APU_CAPABILITY1_IMPLEMENTED UINT32_C(0x01827020)
|
#define RS_APU_CAPABILITY1_IMPLEMENTED UINT32_C(0x01827020)
|
||||||
#define RS_APU_DIGEST_IMPLEMENTED UINT32_C(0x00000000)
|
#define RS_APU_DIGEST_P7_IMPLEMENTED UINT32_C(0xF5005D7C)
|
||||||
#define RS_APU_IRQ_ALL UINT32_C(0x000007FF)
|
#if defined(RS_APU_RELEASE_P7)
|
||||||
|
#define RS_APU_CAPABILITY0_IMPLEMENTED RS_APU_CAPABILITY0_P7_IMPLEMENTED
|
||||||
|
#define RS_APU_DIGEST_IMPLEMENTED RS_APU_DIGEST_P7_IMPLEMENTED
|
||||||
|
#else
|
||||||
|
#define RS_APU_CAPABILITY0_IMPLEMENTED RS_APU_CAPABILITY0_P5_IMPLEMENTED
|
||||||
|
#define RS_APU_DIGEST_IMPLEMENTED UINT32_C(0x00000000)
|
||||||
|
#endif
|
||||||
|
#define RS_APU_IRQ_ALL UINT32_C(0x000007FF)
|
||||||
|
|
||||||
#define RS_APU_REG_IP_ID RS_APU_ABI_IP_ID
|
#define RS_APU_REG_IP_ID RS_APU_ABI_IP_ID
|
||||||
#define RS_APU_REG_IP_VERSION RS_APU_ABI_IP_VERSION
|
#define RS_APU_REG_IP_VERSION RS_APU_ABI_IP_VERSION
|
||||||
#define RS_APU_REG_CAPABILITY0 RS_APU_ABI_CAPABILITY0
|
#define RS_APU_REG_CAPABILITY0 RS_APU_ABI_CAPABILITY0
|
||||||
#define RS_APU_REG_CAPABILITY1 RS_APU_ABI_CAPABILITY1
|
#define RS_APU_REG_CAPABILITY1 RS_APU_ABI_CAPABILITY1
|
||||||
#define RS_APU_REG_ABI_DIGEST RS_APU_ABI_ABI_DIGEST
|
#define RS_APU_REG_ABI_DIGEST RS_APU_ABI_ABI_DIGEST
|
||||||
#define RS_APU_REG(offset) RS_SOC_REG32(RS_SOC_APB4_APU_BASE, (offset))
|
#define RS_APU_REG(offset) RS_SOC_REG32(RS_SOC_APB4_APU_BASE, (offset))
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+33
-5
@@ -623,6 +623,14 @@ the digest includes reserved MP3 identifiers and the P5 rejection/stub contract
|
|||||||
but excludes the inactive P6 design archive. A zero digest means
|
but excludes the inactive P6 design archive. A zero digest means
|
||||||
prototype/incomplete ABI and is not a compatibility hash for a subset.
|
prototype/incomplete ABI and is not a compatibility hash for a subset.
|
||||||
|
|
||||||
|
The P8 digest input is canonical ASCII JSON over sections `discovery`, `apb`,
|
||||||
|
`kws_model`, `formats`, `apumc`, `apum`, `isa`, and `mp3_stub`. Object keys are
|
||||||
|
recursively sorted, separators are literal comma and colon with no whitespace,
|
||||||
|
integers are JSON decimal numbers, non-ASCII text is escaped, and the byte stream
|
||||||
|
has no terminal newline. Apply reflected CRC-32/ISO-HDLC to the complete byte
|
||||||
|
stream. The checked-in tool, handwritten RTL constant, handwritten C constant,
|
||||||
|
and parity test must agree on the exact nonzero result `0xf5005d7c`.
|
||||||
|
|
||||||
Expanded P5 keeps `CAPABILITY0=0x000001bd` and primitive mask `0x001fffff`,
|
Expanded P5 keeps `CAPABILITY0=0x000001bd` and primitive mask `0x001fffff`,
|
||||||
but advertises `CAPABILITY1=0x01827020` and `IP_VERSION=0x00010001` for the
|
but advertises `CAPABILITY1=0x01827020` and `IP_VERSION=0x00010001` for the
|
||||||
32 KiB store and appended PC-high status bit. This hardware discovery is
|
32 KiB store and appended PC-high status bit. This hardware discovery is
|
||||||
@@ -2287,6 +2295,23 @@ scratch reservation. Codec/control-store banks, Gateway A identity, IRQ and
|
|||||||
resource allocations do not change. A new independently arbitrated KWS client
|
resource allocations do not change. A new independently arbitrated KWS client
|
||||||
may access only these KWS banks; it cannot borrow codec buffers or MAC lanes.
|
may access only these KWS banks; it cannot borrow codec buffers or MAC lanes.
|
||||||
|
|
||||||
|
P8 maps each of these sixteen logical banks to one `tc_sram_1024x32` wrapper.
|
||||||
|
Within the APUM and scratch halves, physical bank is byte offset
|
||||||
|
`[4:2] XOR [8:6]` and wrapper word address is byte offset `[14:5]`; this
|
||||||
|
reversible word-interleaving distributes both consecutive and fixed 64-byte
|
||||||
|
stride lanes without changing logical addresses. Every bank accepts at most
|
||||||
|
one read or write per PCLK. The client snapshots one fixed-engine operand
|
||||||
|
vector, coalesces duplicate word reads, issues at most one access per bank per
|
||||||
|
cycle, and stalls the KWS state transition until every synchronous read and
|
||||||
|
required write has retired. A model load is globally idle and has exclusive
|
||||||
|
access; for engine traffic a write wins a same-bank read and the read remains
|
||||||
|
pending. Successful bank activity resets the KWS no-progress watchdog, while a
|
||||||
|
wedged client remains bounded by that watchdog. `HAVE_SRAM_MACRO=NO` uses the
|
||||||
|
same bank selection, one-cycle read latency, arbitration, and stall contract
|
||||||
|
with inferred arrays. This internal scheduling changes no APUM address, tensor,
|
||||||
|
scratch, register, descriptor, result, or error ABI and must re-pass the P7
|
||||||
|
accuracy, cycle, concurrency, xrun, abort, reset, and handoff gates.
|
||||||
|
|
||||||
Scratch layout 1 has A at `0x0000..0x1fff`, B at `0x2000..0x3fff`, MFCC at
|
Scratch layout 1 has A at `0x0000..0x1fff`, B at `0x2000..0x3fff`, MFCC at
|
||||||
`0x4000..0x41ff`, a 50x40 u32 raw-mel ring at `0x4200..0x61ff`, complex FFT
|
`0x4000..0x41ff`, a 50x40 u32 raw-mel ring at `0x4200..0x61ff`, complex FFT
|
||||||
workspace at `0x6200..0x71ff`, 768 S16 ingress samples at `0x7200..0x77ff`,
|
workspace at `0x6200..0x71ff`, 768 S16 ingress samples at `0x7200..0x77ff`,
|
||||||
@@ -3507,7 +3532,7 @@ is changed by these feature evidence requirements.
|
|||||||
| Codecs | WAV/FLAC complete-file differential, sample/rate/channel counts, malformed/truncated/adversarial corpus, metadata limits, FLAC predictor/Rice extremes, and long playlists; MP3 is tested only for unsupported-request/stub compatibility. |
|
| Codecs | WAV/FLAC complete-file differential, sample/rate/channel counts, malformed/truncated/adversarial corpus, metadata limits, FLAC predictor/Rice extremes, and long playlists; MP3 is tested only for unsupported-request/stub compatibility. |
|
||||||
| KWS | MFCC differential, every INT8 layer tensor, converter rejection, official accuracy, threshold/debounce, continuous stream and overrun. |
|
| KWS | MFCC differential, every INT8 layer tensor, converter rejection, official accuracy, threshold/debounce, continuous stream and overrun. |
|
||||||
| SoC | Address/topology, Gateway A fairness, Resource7, IRQ31/PLIC10 exclusion, cache maintenance, handoff, warm flush, HP reset, USB2/SDIO0 contention. |
|
| SoC | Address/topology, Gateway A fairness, Resource7, IRQ31/PLIC10 exclusion, cache maintenance, handoff, warm flush, HP reset, USB2/SDIO0 contention. |
|
||||||
| Software | HAL validation/timeouts/errors, microcode/model load, owner handoff, bare-metal acceptance, and Linux ASoC tests when delivered. |
|
| Software | HAL validation/timeouts/errors, microcode/model load, owner handoff, bare-metal LP/HP acceptance, and cache/handoff tests. Linux ASoC is deferred. |
|
||||||
|
|
||||||
An internal KWS result is not called MLPerf unless the exact applicable rules
|
An internal KWS result is not called MLPerf unless the exact applicable rules
|
||||||
and runner are followed. File interoperability alone is not codec certification.
|
and runner are followed. File interoperability alone is not codec certification.
|
||||||
@@ -3921,14 +3946,17 @@ contention and commercial physical closure remain P8; P6 remains deferred.
|
|||||||
|
|
||||||
ID: `APU-P8`.
|
ID: `APU-P8`.
|
||||||
|
|
||||||
Scope: complete HAL, LP-only startup load, HP ownership/jobs, cache maintenance,
|
Scope: complete freestanding HAL, LP-only startup load, HP ownership/jobs,
|
||||||
Linux ASoC, handoff, USB2/SDIO0 contention, full regression, synthesis recipes,
|
cache maintenance, handoff, USB2/SDIO0 contention, full regression, synthesis
|
||||||
netlist, OpenSTA, warnings/metrics, and commercial-gap report.
|
recipes, netlist, OpenSTA, warnings/metrics, and commercial-gap report. Linux
|
||||||
|
ASoC is deferred because this phase does not freeze a Linux DT binding, kernel
|
||||||
|
driver ABI, or ALSA ownership protocol.
|
||||||
|
|
||||||
Dependencies: Phases1..5 and Phase7. Phase6 remains DEFERRED and is not a
|
Dependencies: Phases1..5 and Phase7. Phase6 remains DEFERRED and is not a
|
||||||
release prerequisite.
|
release prerequisite.
|
||||||
|
|
||||||
Public changes: completes frozen HAL/Linux surfaces; V1 register/descriptor/
|
Public changes: completes frozen HAL and bare-metal LP/HP surfaces; Linux ASoC
|
||||||
|
is not part of this phase. V1 register/descriptor/
|
||||||
microcode/model/address/IRQ/resource/clock allocation cannot change.
|
microcode/model/address/IRQ/resource/clock allocation cannot change.
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# retroSoC APU block-level timing constraints (top: apb4_apu, IHP130 P8 evidence).
|
||||||
|
#
|
||||||
|
# Derived from the SoC core-STA conventions generated by
|
||||||
|
# physical/smoke/sta/opensta/generate_sdc.py out of
|
||||||
|
# rtl/mini/integration/clock_reset_domains.json:
|
||||||
|
# * same 0.2 ns setup / 0.1 ns hold clock uncertainty
|
||||||
|
# * same 0.1 ns clock transition
|
||||||
|
# * same reset handling (false path from the asynchronously-asserted reset)
|
||||||
|
#
|
||||||
|
# Block-specific assumptions (each documented inline):
|
||||||
|
#
|
||||||
|
# 1. Single clock domain. clk_i is the SoC PCLK at 20.833 ns / 48 MHz
|
||||||
|
# (clock_reset_domains.json domain "pclk", period_ns 20.833333333).
|
||||||
|
# Every apb4_apu port is PCLK-synchronous at the SoC boundary:
|
||||||
|
# - the APB4 slave and the AXI4 master attach to PCLK logic; the AXI4
|
||||||
|
# master feeds the PCLK side of the u_io_cdc axi4_async_bridge in
|
||||||
|
# rtl/mini/top/soc_data_plane.sv, so the PCLK/HP crossing is outside
|
||||||
|
# this block;
|
||||||
|
# - all four AXI-Stream interfaces (dma_tx/dma_rx/i2s_tx/i2s_rx) are
|
||||||
|
# clocked by PCLK in rtl/mini/top/apb4_periph.sv (u_*_axis_if .aclk
|
||||||
|
# (clk_i)); the audio-domain crossing lives inside apb4_i2s;
|
||||||
|
# - i2s_tx_underrun_i/i2s_rx_overrun_i/i2s_rx_flush_busy_i are
|
||||||
|
# PCLK-synchronized event outputs of apb4_i2s (cdc_sync/edge-detect in
|
||||||
|
# rtl/ip/serial/apb4_i2s.sv);
|
||||||
|
# - owner_i/owner_lock_i/quiesce_i/resource_reset_i/bridge_epoch_i come
|
||||||
|
# from the PCLK resource-ownership registers.
|
||||||
|
# 2. Reset. rst_n_i is the pclk_rst_n_o output of the SoC rst_sync cell
|
||||||
|
# (asynchronous assert, synchronous deassert), so the reset port is a
|
||||||
|
# false path exactly as in the generated SoC SDC.
|
||||||
|
# 3. I/O budgeting. The SoC core-STA profile false-paths all top I/O because
|
||||||
|
# pad timing is out of scope. For block evidence that would leave every
|
||||||
|
# input-to-register and register-to-output path unconstrained, so this
|
||||||
|
# SDC instead budgets 30% of the PCLK period (6.25 ns) for the external
|
||||||
|
# driver and 30% (6.25 ns) for the external receiver, leaving 40% inside
|
||||||
|
# the block. These budgets are a block-level assumption, not measured SoC
|
||||||
|
# boundary delays; they exist so that no active APU path is left
|
||||||
|
# unconstrained.
|
||||||
|
# 4. External driving cell and output load match the synthesis ABC
|
||||||
|
# constraints (sg13g2_buf_4, 6 fF = 0.006 pF in library units).
|
||||||
|
|
||||||
|
set apu_pclk_period 20.833333333
|
||||||
|
set apu_io_budget 6.25
|
||||||
|
|
||||||
|
set clk_pclk_port [get_ports {clk_i}]
|
||||||
|
if {$clk_pclk_port eq ""} {
|
||||||
|
error "required clock port is missing: clk_i"
|
||||||
|
}
|
||||||
|
create_clock -name clk_pclk -period $apu_pclk_period $clk_pclk_port
|
||||||
|
set_clock_uncertainty -setup 0.2 [get_clocks {clk_pclk}]
|
||||||
|
set_clock_uncertainty -hold 0.1 [get_clocks {clk_pclk}]
|
||||||
|
set_clock_transition 0.1 [get_clocks {clk_pclk}]
|
||||||
|
|
||||||
|
set apu_reset_port [get_ports {rst_n_i}]
|
||||||
|
if {$apu_reset_port eq ""} {
|
||||||
|
error "required reset port is missing: rst_n_i"
|
||||||
|
}
|
||||||
|
set_false_path -from $apu_reset_port
|
||||||
|
|
||||||
|
set apu_inputs [get_ports -quiet -filter {name != clk_i && name != rst_n_i} [all_inputs]]
|
||||||
|
set_input_delay -clock clk_pclk $apu_io_budget $apu_inputs
|
||||||
|
set_output_delay -clock clk_pclk $apu_io_budget [all_outputs]
|
||||||
|
set_driving_cell -lib_cell sg13g2_buf_4 -pin X $apu_inputs
|
||||||
|
set_load 0.006 [all_outputs]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# retroSoC APU block-level OpenSTA run (top: apb4_apu), derived from opensta.tcl.
|
||||||
|
# Reports max-delay and min-delay (hold) results separately, records the same
|
||||||
|
# wns/tns metric format as the SoC flow, and adds an explicit check_timing
|
||||||
|
# inventory so unconstrained endpoints are visible in the evidence set.
|
||||||
|
|
||||||
|
foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_SDC OPENSTA_REPORT \
|
||||||
|
OPENSTA_REPORT_MIN OPENSTA_WORST OPENSTA_METRICS OPENSTA_CHECK_TIMING \
|
||||||
|
OPENSTA_CHECK_TYPES} {
|
||||||
|
if {![info exists ::env($variable)] || $::env($variable) eq ""} {
|
||||||
|
error "required environment variable is missing: $variable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read_liberty $::env(OPENSTA_LIBERTY)
|
||||||
|
if {[info exists ::env(OPENSTA_SRAM_LIBS)] && $::env(OPENSTA_SRAM_LIBS) ne ""} {
|
||||||
|
foreach liberty $::env(OPENSTA_SRAM_LIBS) {
|
||||||
|
read_liberty $liberty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_verilog $::env(OPENSTA_NETLIST)
|
||||||
|
link_design apb4_apu
|
||||||
|
read_sdc $::env(OPENSTA_SDC)
|
||||||
|
|
||||||
|
# max-delay (setup) violations; empty report means no negative-slack path
|
||||||
|
report_checks -path_delay max -sort_by_slack -slack_max 0.0 \
|
||||||
|
-group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_REPORT)
|
||||||
|
|
||||||
|
# min-delay (hold) violations, reported separately per docs/ip/apu.md
|
||||||
|
report_checks -path_delay min -sort_by_slack -slack_max 0.0 \
|
||||||
|
-group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_REPORT_MIN)
|
||||||
|
|
||||||
|
# worst paths regardless of sign: 20 worst max-delay and 20 worst min-delay
|
||||||
|
set worst_file $::env(OPENSTA_WORST)
|
||||||
|
report_checks -path_delay max -sort_by_slack -format full \
|
||||||
|
-group_path_count 20 -endpoint_path_count 1 > $worst_file
|
||||||
|
report_checks -path_delay min -sort_by_slack -format full \
|
||||||
|
-group_path_count 20 -endpoint_path_count 1 >> $worst_file
|
||||||
|
|
||||||
|
# unconstrained-path inventory: only paths carrying the explicit
|
||||||
|
# "(Path is unconstrained)" marker count as unconstrained evidence
|
||||||
|
report_checks -unconstrained -sort_by_slack \
|
||||||
|
-group_path_count 1000 -endpoint_path_count 1000 > $::env(OPENSTA_CHECK_TIMING)
|
||||||
|
|
||||||
|
# design-rule inventory (slew/fanout/capacitance/pulse-width), report-only
|
||||||
|
report_check_types > $::env(OPENSTA_CHECK_TYPES)
|
||||||
|
|
||||||
|
set metrics_file $::env(OPENSTA_METRICS)
|
||||||
|
set metrics_tmp "${metrics_file}.tmp"
|
||||||
|
set metrics [open $metrics_file "w"]
|
||||||
|
|
||||||
|
report_wns -min > $metrics_tmp
|
||||||
|
set input [open $metrics_tmp "r"]
|
||||||
|
puts $metrics "wns_min=[string trim [read $input]]"
|
||||||
|
close $input
|
||||||
|
|
||||||
|
report_wns -max > $metrics_tmp
|
||||||
|
set input [open $metrics_tmp "r"]
|
||||||
|
puts $metrics "wns_max=[string trim [read $input]]"
|
||||||
|
close $input
|
||||||
|
|
||||||
|
report_tns -min > $metrics_tmp
|
||||||
|
set input [open $metrics_tmp "r"]
|
||||||
|
puts $metrics "tns_min=[string trim [read $input]]"
|
||||||
|
close $input
|
||||||
|
|
||||||
|
report_tns -max > $metrics_tmp
|
||||||
|
set input [open $metrics_tmp "r"]
|
||||||
|
puts $metrics "tns_max=[string trim [read $input]]"
|
||||||
|
close $input
|
||||||
|
|
||||||
|
close $metrics
|
||||||
|
file delete -force $metrics_tmp
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export the APU block-level Yosys synthesis filelist (apb4_apu as top).
|
||||||
|
|
||||||
|
The filelist is the exact subset of the whole-SoC synthesis inputs that the
|
||||||
|
APU block needs: the APU sources from rtl/mini/filelist/ip.fl, the three
|
||||||
|
clusterip interface definitions used in the APU port list, the tc_sram
|
||||||
|
wrapper from rtl/mini/filelist/tech.fl, and the include directories from
|
||||||
|
rtl/mini/filelist/inc.fl. PDK filelists are excluded, matching comb.py in
|
||||||
|
the whole-SoC flow: SRAM macros arrive as Liberty black boxes through
|
||||||
|
read_liberty -lib in init_tech.tcl, never as behavioral Verilog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SCRIPT_DIR = REPO_ROOT / "rtl/mini/script"
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
from filelist import FileList, parse_filelists, write_filelist # noqa: E402
|
||||||
|
from generate_filelist import generate_all # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
APU_SOURCE_MARKERS = ("/ip/multimedia/apu_", "/ip/multimedia/apb4_apu.sv")
|
||||||
|
INTERFACE_SOURCES = ("apb4_if.sv", "axi4_if.sv", "axi4_stream_if.sv")
|
||||||
|
# clusterip utility cells used inside the APU RTL: register.sv provides the
|
||||||
|
# dff/dffr/dffrc register cells; soc_common_fifo.sv includes fifo.sv for the
|
||||||
|
# stream_fifo used by the stream router and primitive dispatcher.
|
||||||
|
UTILITY_SOURCES = ("register.sv", "soc_common_fifo.sv")
|
||||||
|
# DMA engine frontend instantiated by apu_dma.sv.
|
||||||
|
IP_SOURCES = ("dma_axi4_master.sv",)
|
||||||
|
TECH_SOURCES = ("tc_sram.sv",)
|
||||||
|
|
||||||
|
|
||||||
|
def select_sources(paths: list[Path], names: tuple[str, ...], origin: str) -> list[Path]:
|
||||||
|
selected = [path for path in paths if path.name in names]
|
||||||
|
missing = [name for name in names if name not in {path.name for path in selected}]
|
||||||
|
if missing:
|
||||||
|
raise FileNotFoundError(f"{origin} does not provide: {', '.join(missing)}")
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def select_apu_sources(paths: list[Path]) -> list[Path]:
|
||||||
|
selected = [
|
||||||
|
path
|
||||||
|
for path in paths
|
||||||
|
if any(marker in path.as_posix() for marker in APU_SOURCE_MARKERS)
|
||||||
|
]
|
||||||
|
names = {path.name for path in selected}
|
||||||
|
if "apb4_apu.sv" not in names or "apu_microcode_pkg.sv" not in names:
|
||||||
|
raise FileNotFoundError("ip.fl does not provide the complete APU source set")
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def build_block_filelist(defines: list[str], generated_dir: Path) -> FileList:
|
||||||
|
includes = parse_filelists([generated_dir / "inc.fl"])
|
||||||
|
interfaces = parse_filelists([generated_dir / "commonip.fl"])
|
||||||
|
tech = parse_filelists([generated_dir / "tech.fl"])
|
||||||
|
ip = parse_filelists([generated_dir / "ip.fl"])
|
||||||
|
|
||||||
|
block = FileList()
|
||||||
|
block.defines = list(defines)
|
||||||
|
block.incdirs = list(includes.incdirs)
|
||||||
|
block.files = [
|
||||||
|
*select_sources(interfaces.files, INTERFACE_SOURCES, "commonip.fl"),
|
||||||
|
*select_sources(interfaces.files, UTILITY_SOURCES, "commonip.fl"),
|
||||||
|
*select_sources(tech.files, TECH_SOURCES, "tech.fl"),
|
||||||
|
*select_sources(ip.files, IP_SOURCES, "ip.fl"),
|
||||||
|
*select_apu_sources(ip.files),
|
||||||
|
]
|
||||||
|
return block.deduplicate()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--define",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="filelist +define+ token, same convention as generate_filelist.py",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
invalid = [item for item in args.define if not item.startswith("+define+")]
|
||||||
|
if invalid:
|
||||||
|
raise SystemExit(f"invalid define token(s): {' '.join(invalid)}")
|
||||||
|
if not any(item == "+define+PDK_IHP130" for item in args.define):
|
||||||
|
raise SystemExit("the APU block flow is locked to +define+PDK_IHP130")
|
||||||
|
if not any(item == "+define+HAVE_SRAM_MACRO" for item in args.define):
|
||||||
|
raise SystemExit("the APU block flow requires +define+HAVE_SRAM_MACRO")
|
||||||
|
output_dir = args.output_dir.resolve()
|
||||||
|
generated_dir = output_dir / "templates"
|
||||||
|
generate_all(generated_dir, args.define)
|
||||||
|
filelist = build_block_filelist(args.define, generated_dir)
|
||||||
|
output = output_dir / "apu_block.fl"
|
||||||
|
write_filelist(output, filelist)
|
||||||
|
print(f"generated APU block filelist: {output} ({len(filelist.files)} sources)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# APU block-level IHP130 synthesis + STA evidence flow (docs/ip/apu.md P8).
|
||||||
|
#
|
||||||
|
# Top: apb4_apu with parameter EnableP7 selected by APU_BLOCK_ENABLE_P7=0|1.
|
||||||
|
# Synthesis mirrors the whole-SoC Yosys flow (same script passes, same locked
|
||||||
|
# tools) with measured ABC deviations: the block recipe drops the "&fraig -x"
|
||||||
|
# SAT-sweeping pass (script/abc_balanced_apu_block.script), which does not
|
||||||
|
# terminate on the APU multiplier-dominated AIGs (14+ h on apu_kernel_engine
|
||||||
|
# alone with the stock recipe), and the giant KWS register-file window module
|
||||||
|
# is mapped in a second ABC phase with script/abc_balanced_apu_block_kws.script
|
||||||
|
# (block recipe minus "&dch -f", which likewise does not terminate on its
|
||||||
|
# 36.7M-AND network; measured 20.4 GB peak RSS, versus ~208 GB orphans with
|
||||||
|
# the stock recipe). STA runs OpenSTA at the 20.833 ns PCLK target
|
||||||
|
# with the block SDC in physical/smoke/sta/opensta/apu_block.sdc.
|
||||||
|
#
|
||||||
|
# Standalone use (until the root Makefile wires the include):
|
||||||
|
# make -f physical/smoke/syn/yosys/apu_block.mk CONFIG=configs/ci/ihp130.mk \
|
||||||
|
# apu-block-evidence
|
||||||
|
# Root-Makefile integration: include this file after yosys.mk/opensta.mk.
|
||||||
|
|
||||||
|
ifeq ($(origin ROOT_PATH),undefined)
|
||||||
|
ROOT_PATH := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../../../..)
|
||||||
|
SYNTH := YOSYS
|
||||||
|
STA := OPENSTA
|
||||||
|
include $(ROOT_PATH)/Makefile
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Sub-makes must reload this file in standalone mode, or the root Makefile
|
||||||
|
# once this file is wired into it.
|
||||||
|
APU_BLOCK_SELF := $(firstword $(MAKEFILE_LIST))
|
||||||
|
|
||||||
|
ifeq ($(origin OPENSTA_LIBERTY),undefined)
|
||||||
|
include $(ROOT_PATH)/physical/smoke/sta/opensta/pdk_timing.mk
|
||||||
|
endif
|
||||||
|
|
||||||
|
APU_BLOCK_ENABLE_P7 ?= 1
|
||||||
|
APU_BLOCK_TOP := apb4_apu
|
||||||
|
APU_BLOCK_TAG := p$(APU_BLOCK_ENABLE_P7)-$(SYNTH_RECIPE)
|
||||||
|
APU_BLOCK_SYN := $(VARIANT_ROOT)/syn/apu-block-$(APU_BLOCK_TAG)
|
||||||
|
APU_BLOCK_STA := $(VARIANT_ROOT)/sta/apu-block-$(APU_BLOCK_TAG)
|
||||||
|
APU_BLOCK_OUT := $(APU_BLOCK_SYN)/out
|
||||||
|
APU_BLOCK_TMP := $(APU_BLOCK_SYN)/tmp
|
||||||
|
APU_BLOCK_RPT := $(APU_BLOCK_SYN)/rpt
|
||||||
|
APU_BLOCK_FL_DIR := $(APU_BLOCK_SYN)/filelists
|
||||||
|
APU_BLOCK_FL := $(APU_BLOCK_FL_DIR)/apu_block.fl
|
||||||
|
|
||||||
|
APU_BLOCK_NETLIST := $(APU_BLOCK_OUT)/$(APU_BLOCK_TOP)_yosys.v
|
||||||
|
APU_BLOCK_CONFIG := $(APU_BLOCK_OUT)/$(APU_BLOCK_TOP)_yosys.config
|
||||||
|
APU_BLOCK_EXPORTER := $(ROOT_PATH)/physical/smoke/syn/tools/export_apu_block_filelist.py
|
||||||
|
APU_BLOCK_SYNTH_TCL := $(ROOT_PATH)/physical/smoke/syn/yosys/script/apu_block_synth.tcl
|
||||||
|
APU_BLOCK_STA_TCL := $(ROOT_PATH)/physical/smoke/sta/opensta/apu_block_opensta.tcl
|
||||||
|
APU_BLOCK_SDC := $(ROOT_PATH)/physical/smoke/sta/opensta/apu_block.sdc
|
||||||
|
APU_BLOCK_REPORT_PY := $(ROOT_PATH)/physical/smoke/syn/yosys/report_apu_block.py
|
||||||
|
APU_BLOCK_REPORT_DIR := $(VARIANT_ROOT)/syn/apu-block-$(SYNTH_RECIPE)
|
||||||
|
|
||||||
|
APU_BLOCK_YOSYS_SCRIPTS := $(APU_BLOCK_SYNTH_TCL) \
|
||||||
|
$(ROOT_PATH)/physical/smoke/syn/yosys/script/common.tcl \
|
||||||
|
$(ROOT_PATH)/physical/smoke/syn/yosys/script/init_tech.tcl \
|
||||||
|
$(ROOT_PATH)/physical/smoke/syn/yosys/script/abc_$(SYNTH_RECIPE).script \
|
||||||
|
$(ROOT_PATH)/physical/smoke/syn/yosys/script/abc_$(SYNTH_RECIPE)_apu_block_kws.script \
|
||||||
|
$(wildcard $(ROOT_PATH)/physical/smoke/syn/yosys/script/abc_$(SYNTH_RECIPE)_apu_block.script) \
|
||||||
|
$(wildcard $(ROOT_PATH)/physical/smoke/syn/yosys/script/abc_$(SYNTH_RECIPE)_apu_block.script)
|
||||||
|
|
||||||
|
# APU hierarchy accounting keep list: SoC-style slang variant patterns plus
|
||||||
|
# the plain type patterns; union keeps every apu_* submodule and tc_sram
|
||||||
|
# wrapper hierarchical for per-block cell/macro accounting.
|
||||||
|
APU_BLOCK_KEEP_HIER := "t:tc_sram*$$*" "t:apu_*$$*" "t:tc_sram*" "t:apu_*"
|
||||||
|
|
||||||
|
# The block runs at the PCLK target only; derive 20833 ps from the locked
|
||||||
|
# clock/reset inventory instead of typing the number here.
|
||||||
|
ifeq ($(origin APU_BLOCK_TARGET_PERIOD_PS),undefined)
|
||||||
|
APU_BLOCK_TARGET_PERIOD_PS := $(shell python3 $(ROOT_PATH)/scripts/yosys_period.py \
|
||||||
|
--domains $(ROOT_PATH)/rtl/mini/integration/clock_reset_domains.json --domain pclk)
|
||||||
|
ifeq ($(strip $(APU_BLOCK_TARGET_PERIOD_PS)),)
|
||||||
|
$(error Failed to derive APU_BLOCK_TARGET_PERIOD_PS from the clock/reset inventory)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
OPENSTA ?= sta
|
||||||
|
OPENSTA_THREADS ?= $(JOBS)
|
||||||
|
APU_BLOCK_REPORT := $(APU_BLOCK_STA)/apu_block_sta_max.log
|
||||||
|
APU_BLOCK_REPORT_MIN := $(APU_BLOCK_STA)/apu_block_sta_min.log
|
||||||
|
APU_BLOCK_WORST := $(APU_BLOCK_STA)/apu_block_worst.log
|
||||||
|
APU_BLOCK_CHECK := $(APU_BLOCK_STA)/unconstrained.rpt
|
||||||
|
APU_BLOCK_CHECK_TYPES := $(APU_BLOCK_STA)/check_types.rpt
|
||||||
|
APU_BLOCK_METRICS := $(APU_BLOCK_STA)/timing_metrics.rpt
|
||||||
|
APU_BLOCK_STA_LOG := $(APU_BLOCK_STA)/opensta.log
|
||||||
|
APU_BLOCK_STA_RESULT := $(APU_BLOCK_STA)/result-sta.json
|
||||||
|
APU_BLOCK_SYN_RESULT := $(APU_BLOCK_SYN)/result-synth.json
|
||||||
|
|
||||||
|
$(APU_BLOCK_FL): $(APU_BLOCK_EXPORTER) $(FILELIST_TEMPLATES) \
|
||||||
|
$(RTL_PATH)/script/generate_filelist.py $(RTL_PATH)/script/filelist.py
|
||||||
|
@mkdir -p $(APU_BLOCK_FL_DIR)
|
||||||
|
python3 $(APU_BLOCK_EXPORTER) --output-dir $(APU_BLOCK_FL_DIR) \
|
||||||
|
$(foreach define,$(DEF_LIST),--define $(define))
|
||||||
|
|
||||||
|
apu-block-filelist: $(APU_BLOCK_FL)
|
||||||
|
|
||||||
|
$(APU_BLOCK_NETLIST): $(APU_BLOCK_FL) $(APU_BLOCK_YOSYS_SCRIPTS)
|
||||||
|
@mkdir -p $(APU_BLOCK_OUT)
|
||||||
|
@mkdir -p $(APU_BLOCK_TMP)
|
||||||
|
@mkdir -p $(APU_BLOCK_RPT)
|
||||||
|
python3 $(ROOT_PATH)/scripts/run_flow.py --tool yosys \
|
||||||
|
--log $(APU_BLOCK_SYN)/$(APU_BLOCK_TOP).log --result $(APU_BLOCK_SYN_RESULT) \
|
||||||
|
--env PDK=$(PDK) --env SOC=$(SOC) --env SYNTH_RECIPE=$(SYNTH_RECIPE) \
|
||||||
|
--env HAVE_SRAM_MACRO=$(HAVE_SRAM_MACRO) --env SRAM_SIZE_KIB=$(SRAM_SIZE_KIB) \
|
||||||
|
--env YOSYS_TARGET_PERIOD_PS=$(APU_BLOCK_TARGET_PERIOD_PS) \
|
||||||
|
--env SV_FLIST=$(APU_BLOCK_FL) --env TOP_DESIGN=$(APU_BLOCK_TOP) \
|
||||||
|
--env CONFIG=$(APU_BLOCK_CONFIG) --env APU_ENABLE_P7=$(APU_BLOCK_ENABLE_P7) \
|
||||||
|
--env YOSYS_FLATTEN_HIER=1 --env 'YOSYS_KEEP_HIER_INST=$(APU_BLOCK_KEEP_HIER)' \
|
||||||
|
--env PROJ_NAME=$(APU_BLOCK_TOP) --env WORK=$(APU_BLOCK_TMP) \
|
||||||
|
--env BUILD=$(APU_BLOCK_OUT) --env REPORTS=$(APU_BLOCK_RPT) \
|
||||||
|
--env NETLIST=$(APU_BLOCK_NETLIST) -- \
|
||||||
|
yosys -c $(APU_BLOCK_SYNTH_TCL)
|
||||||
|
|
||||||
|
apu-block-synth: $(APU_BLOCK_NETLIST)
|
||||||
|
|
||||||
|
apu-block-sta: $(APU_BLOCK_NETLIST) $(APU_BLOCK_SDC) $(APU_BLOCK_STA_TCL)
|
||||||
|
@mkdir -p $(APU_BLOCK_STA)
|
||||||
|
@for input in $(APU_BLOCK_NETLIST) $(OPENSTA_LIBERTY) $(OPENSTA_SRAM_LIBS) \
|
||||||
|
$(APU_BLOCK_SDC) $(APU_BLOCK_CONFIG); do \
|
||||||
|
test -f "$$input" || { echo "OpenSTA input missing: $$input" >&2; exit 1; }; \
|
||||||
|
done
|
||||||
|
@grep -qx 'PDK=$(PDK)' $(APU_BLOCK_CONFIG) || { \
|
||||||
|
echo "OpenSTA netlist configuration does not match PDK=$(PDK); rerun synthesis" >&2; \
|
||||||
|
exit 1; }
|
||||||
|
@grep -qx 'APU_ENABLE_P7=$(APU_BLOCK_ENABLE_P7)' $(APU_BLOCK_CONFIG) || { \
|
||||||
|
echo "OpenSTA netlist APU_ENABLE_P7=$(APU_BLOCK_ENABLE_P7) mismatch; rerun synthesis" >&2; \
|
||||||
|
exit 1; }
|
||||||
|
@grep -qx 'HAVE_SRAM_MACRO=$(HAVE_SRAM_MACRO)' $(APU_BLOCK_CONFIG) || { \
|
||||||
|
echo "OpenSTA netlist SRAM macro configuration mismatch; rerun synthesis" >&2; \
|
||||||
|
exit 1; }
|
||||||
|
python3 $(ROOT_PATH)/scripts/run_flow.py --tool opensta --log $(APU_BLOCK_STA_LOG) \
|
||||||
|
--result $(APU_BLOCK_STA_RESULT) \
|
||||||
|
--env OPENSTA_NETLIST=$(APU_BLOCK_NETLIST) --env OPENSTA_LIBERTY=$(OPENSTA_LIBERTY) \
|
||||||
|
--env 'OPENSTA_SRAM_LIBS=$(OPENSTA_SRAM_LIBS)' \
|
||||||
|
--env OPENSTA_SDC=$(APU_BLOCK_SDC) --env OPENSTA_REPORT=$(APU_BLOCK_REPORT) \
|
||||||
|
--env OPENSTA_REPORT_MIN=$(APU_BLOCK_REPORT_MIN) --env OPENSTA_WORST=$(APU_BLOCK_WORST) \
|
||||||
|
--env OPENSTA_METRICS=$(APU_BLOCK_METRICS) --env OPENSTA_CHECK_TIMING=$(APU_BLOCK_CHECK) \
|
||||||
|
--env OPENSTA_CHECK_TYPES=$(APU_BLOCK_CHECK_TYPES) -- \
|
||||||
|
$(OPENSTA) -no_init -exit -threads $(OPENSTA_THREADS) $(APU_BLOCK_STA_TCL)
|
||||||
|
|
||||||
|
apu-block-report:
|
||||||
|
python3 $(APU_BLOCK_REPORT_PY) \
|
||||||
|
--baseline-synth $(VARIANT_ROOT)/syn/apu-block-p0-$(SYNTH_RECIPE) \
|
||||||
|
--expanded-synth $(VARIANT_ROOT)/syn/apu-block-p1-$(SYNTH_RECIPE) \
|
||||||
|
--baseline-sta $(VARIANT_ROOT)/sta/apu-block-p0-$(SYNTH_RECIPE) \
|
||||||
|
--expanded-sta $(VARIANT_ROOT)/sta/apu-block-p1-$(SYNTH_RECIPE) \
|
||||||
|
--output-dir $(APU_BLOCK_REPORT_DIR) --recipe $(SYNTH_RECIPE)
|
||||||
|
|
||||||
|
apu-block-evidence:
|
||||||
|
$(MAKE) -f $(APU_BLOCK_SELF) APU_BLOCK_ENABLE_P7=0 apu-block-synth apu-block-sta
|
||||||
|
$(MAKE) -f $(APU_BLOCK_SELF) APU_BLOCK_ENABLE_P7=1 apu-block-synth apu-block-sta
|
||||||
|
$(MAKE) -f $(APU_BLOCK_SELF) apu-block-report
|
||||||
|
|
||||||
|
apu-block-clean:
|
||||||
|
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(VARIANT_ROOT)/syn/apu-block-p0-$(SYNTH_RECIPE)
|
||||||
|
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(VARIANT_ROOT)/syn/apu-block-p1-$(SYNTH_RECIPE)
|
||||||
|
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(VARIANT_ROOT)/sta/apu-block-p0-$(SYNTH_RECIPE)
|
||||||
|
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(VARIANT_ROOT)/sta/apu-block-p1-$(SYNTH_RECIPE)
|
||||||
|
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(APU_BLOCK_REPORT_DIR)
|
||||||
|
|
||||||
|
.PHONY: apu-block-filelist apu-block-synth apu-block-sta apu-block-report \
|
||||||
|
apu-block-evidence apu-block-clean
|
||||||
@@ -0,0 +1,751 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the retroSoC APU block-level synthesis/STA evidence report.
|
||||||
|
|
||||||
|
Parses the named artifacts of two APU block configurations (EnableP7=0
|
||||||
|
baseline and EnableP7=1 expanded) produced by physical/smoke/syn/yosys/
|
||||||
|
apu_block.mk and emits:
|
||||||
|
|
||||||
|
* apu-block-synth.json -- machine-readable evidence
|
||||||
|
* apu-block-synth.md -- human-readable summary
|
||||||
|
|
||||||
|
Every number is parsed from a named artifact (Yosys stat JSON, netlist,
|
||||||
|
inventory reports, OpenSTA reports, RTL source constants); nothing is
|
||||||
|
hand-typed. Standard-cell area is report-only: no area or power ceiling is
|
||||||
|
evaluated, and pre-layout STA is not post-layout signoff.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
TOP_MODULE = "apb4_apu"
|
||||||
|
|
||||||
|
MACRO_TYPE_PREFIX = "RM_IHPSG13_"
|
||||||
|
STDCELL_TYPE_PREFIX = "sg13g2_"
|
||||||
|
SRAM_WRAPPER_PREFIX = "tc_sram"
|
||||||
|
CLOCK_GATE_RE = re.compile(r"^sg13g2_(s)?lgcp_\d+$")
|
||||||
|
|
||||||
|
MACRO_CLASS_BY_ANCESTOR = {
|
||||||
|
"u_control_store": "control_store",
|
||||||
|
"u_local_sram": "local_data",
|
||||||
|
"u_microcode_loader": "loader_path_stack",
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCAL_SRAM_RTL = REPO_ROOT / "rtl/ip/multimedia/apu_local_sram.sv"
|
||||||
|
APU_TOP_RTL = REPO_ROOT / "rtl/ip/multimedia/apb4_apu.sv"
|
||||||
|
APU_DEFINE_RTL = REPO_ROOT / "rtl/ip/multimedia/apu_define.svh"
|
||||||
|
DOC_REFERENCE = "docs/ip/apu.md:3536-3541"
|
||||||
|
|
||||||
|
MODULE_DEF_RE = re.compile(r"^module\s+\\?([^\s(]+)", re.MULTILINE)
|
||||||
|
WORST_BLOCK_RE = re.compile(r"(?=^Startpoint: )", re.MULTILINE)
|
||||||
|
UNCONSTRAINED_MARKER = "(Path is unconstrained)"
|
||||||
|
VIOLATED_MARKER = "(VIOLATED)"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_unconstrained(path: Path) -> dict[str, Any]:
|
||||||
|
text = read_text(path)
|
||||||
|
endpoints: list[str] = []
|
||||||
|
for block in WORST_BLOCK_RE.split(text):
|
||||||
|
if not block.startswith("Startpoint: ") or UNCONSTRAINED_MARKER not in block:
|
||||||
|
continue
|
||||||
|
endpoint = re.search(r"^Endpoint: (\S+)", block, re.MULTILINE)
|
||||||
|
endpoints.append(endpoint.group(1) if endpoint else "<unknown>")
|
||||||
|
return {
|
||||||
|
"unconstrained_path_count": text.count(UNCONSTRAINED_MARKER),
|
||||||
|
"unconstrained_endpoints": sorted(set(endpoints)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def count_violations(path: Path) -> int:
|
||||||
|
return read_text(path).count(VIOLATED_MARKER)
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path: Path) -> Any:
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f"missing artifact: {path}")
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise SystemExit(f"invalid JSON artifact {path}: {error}") from error
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path: Path) -> str:
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f"missing artifact: {path}")
|
||||||
|
return path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config(path: Path) -> dict[str, str]:
|
||||||
|
config: dict[str, str] = {}
|
||||||
|
for line in read_text(path).splitlines():
|
||||||
|
name, separator, value = line.partition("=")
|
||||||
|
if separator:
|
||||||
|
config[name.strip()] = value.strip()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def parse_metrics(path: Path) -> dict[str, float]:
|
||||||
|
metrics: dict[str, float] = {}
|
||||||
|
for line in read_text(path).splitlines():
|
||||||
|
name, separator, value = line.partition("=")
|
||||||
|
if not separator:
|
||||||
|
continue
|
||||||
|
# values carry the report prefix, e.g. "wns max -4262.54"
|
||||||
|
token = value.strip().split()[-1] if value.strip() else ""
|
||||||
|
try:
|
||||||
|
metrics[name.strip()] = float(token)
|
||||||
|
except ValueError:
|
||||||
|
metrics[name.strip()] = float("nan")
|
||||||
|
for key in ("wns_min", "wns_max", "tns_min", "tns_max"):
|
||||||
|
if key not in metrics:
|
||||||
|
raise SystemExit(f"timing metric {key} missing in {path}")
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def count_report_lines(path: Path) -> int:
|
||||||
|
return sum(1 for line in read_text(path).splitlines() if line.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def split_module_name(name: str) -> tuple[str, str]:
|
||||||
|
clean = name.lstrip("\\")
|
||||||
|
if "$" not in clean:
|
||||||
|
return clean, ""
|
||||||
|
module_type, path = clean.split("$", 1)
|
||||||
|
return module_type, path
|
||||||
|
|
||||||
|
|
||||||
|
def classify_cell_type(cell_type: str, defined_modules: set[str]) -> str:
|
||||||
|
if cell_type in defined_modules:
|
||||||
|
return "submodule"
|
||||||
|
if cell_type.startswith(STDCELL_TYPE_PREFIX):
|
||||||
|
return "pdk_stdcell"
|
||||||
|
if cell_type.startswith(MACRO_TYPE_PREFIX):
|
||||||
|
return "pdk_macro"
|
||||||
|
if cell_type.startswith("$_"):
|
||||||
|
return "internal_unmapped"
|
||||||
|
return "unresolved_non_pdk"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Node:
|
||||||
|
path: str
|
||||||
|
module_type: str
|
||||||
|
own_cells: int = 0
|
||||||
|
own_std_cells: int = 0
|
||||||
|
own_macro_cells: int = 0
|
||||||
|
own_area: float = 0.0
|
||||||
|
children: dict[str, "Node"] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def instance(self) -> str:
|
||||||
|
return self.path.rsplit(".", 1)[-1] if self.path else TOP_MODULE
|
||||||
|
|
||||||
|
def subtree(self) -> tuple[int, int, float]:
|
||||||
|
cells = self.own_cells
|
||||||
|
macros = self.own_macro_cells
|
||||||
|
area = self.own_area
|
||||||
|
for child in self.children.values():
|
||||||
|
child_cells, child_macros, child_area = child.subtree()
|
||||||
|
cells += child_cells
|
||||||
|
macros += child_macros
|
||||||
|
area += child_area
|
||||||
|
return cells, macros, area
|
||||||
|
|
||||||
|
|
||||||
|
def build_tree(area_json: dict[str, Any], defined_modules: set[str]) -> tuple[Node, set[str]]:
|
||||||
|
root = Node(path="", module_type=TOP_MODULE)
|
||||||
|
cell_types: set[str] = set()
|
||||||
|
modules = area_json.get("modules")
|
||||||
|
if not isinstance(modules, dict):
|
||||||
|
raise SystemExit("area JSON has no modules section")
|
||||||
|
entries = []
|
||||||
|
for raw_name, stats in modules.items():
|
||||||
|
module_type, path = split_module_name(raw_name)
|
||||||
|
if path and not path.startswith(f"{TOP_MODULE}."):
|
||||||
|
raise SystemExit(f"module outside {TOP_MODULE} hierarchy: {raw_name}")
|
||||||
|
relative = path[len(TOP_MODULE) + 1 :] if path else ""
|
||||||
|
entries.append((relative, module_type, stats))
|
||||||
|
# parents must be attached before their children
|
||||||
|
entries.sort(key=lambda item: item[0].count("."))
|
||||||
|
for relative, module_type, stats in entries:
|
||||||
|
by_type = stats.get("num_cells_by_type", {})
|
||||||
|
node = Node(path=relative, module_type=module_type)
|
||||||
|
for cell_type, count in by_type.items():
|
||||||
|
cell_types.add(cell_type)
|
||||||
|
if classify_cell_type(cell_type, defined_modules) == "submodule":
|
||||||
|
continue
|
||||||
|
node.own_cells += int(count)
|
||||||
|
if cell_type.startswith(MACRO_TYPE_PREFIX):
|
||||||
|
node.own_macro_cells += int(count)
|
||||||
|
else:
|
||||||
|
node.own_std_cells += int(count)
|
||||||
|
node.own_area = float(stats.get("area", 0.0))
|
||||||
|
parent = root
|
||||||
|
if relative:
|
||||||
|
segments = relative.split(".")
|
||||||
|
walked: list[str] = []
|
||||||
|
for segment in segments[:-1]:
|
||||||
|
walked.append(segment)
|
||||||
|
parent = parent.children.setdefault(
|
||||||
|
".".join(walked), Node(path=".".join(walked), module_type="<missing>")
|
||||||
|
)
|
||||||
|
placeholder = parent.children.get(relative)
|
||||||
|
if placeholder is not None:
|
||||||
|
node.children = placeholder.children
|
||||||
|
parent.children[relative] = node
|
||||||
|
return root, cell_types
|
||||||
|
|
||||||
|
|
||||||
|
def parse_macros(path: Path) -> dict[str, Any]:
|
||||||
|
by_class: dict[str, int] = {name: 0 for name in MACRO_CLASS_BY_ANCESTOR.values()}
|
||||||
|
instances: list[str] = []
|
||||||
|
for line in read_text(path).splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
instances.append(line)
|
||||||
|
cell_path = line.replace("/", ".")
|
||||||
|
for ancestor, klass in MACRO_CLASS_BY_ANCESTOR.items():
|
||||||
|
if f".{ancestor}." in cell_path:
|
||||||
|
by_class[klass] += 1
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
by_class.setdefault("unclassified", 0)
|
||||||
|
by_class["unclassified"] += 1
|
||||||
|
return {"total_physical": len(instances), "by_class": by_class, "instances": instances}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rtl_constants() -> dict[str, int]:
|
||||||
|
constants: dict[str, int] = {}
|
||||||
|
local_sram = read_text(LOCAL_SRAM_RTL)
|
||||||
|
match = re.search(r"localparam\s+int\s+unsigned\s+LogicalBankCount\s*=\s*(\d+)", local_sram)
|
||||||
|
if not match:
|
||||||
|
raise SystemExit(f"LogicalBankCount not found in {LOCAL_SRAM_RTL}")
|
||||||
|
constants["local_data_logical_banks"] = int(match.group(1))
|
||||||
|
|
||||||
|
top_rtl = read_text(APU_TOP_RTL)
|
||||||
|
depth = re.search(
|
||||||
|
r"apu_control_store\s*#\s*\(\s*\.Depth\((\d+)\)\s*\)\s*u_control_store", top_rtl
|
||||||
|
)
|
||||||
|
if not depth:
|
||||||
|
raise SystemExit(f"control-store Depth not found in {APU_TOP_RTL}")
|
||||||
|
constants["control_store_depth"] = int(depth.group(1))
|
||||||
|
stack = re.search(r"\.PathStackDepth\((\d+)\)", top_rtl)
|
||||||
|
if not stack:
|
||||||
|
raise SystemExit(f"PathStackDepth not found in {APU_TOP_RTL}")
|
||||||
|
constants["path_stack_depth"] = int(stack.group(1))
|
||||||
|
|
||||||
|
define_rtl = read_text(APU_DEFINE_RTL)
|
||||||
|
for key, macro in (
|
||||||
|
("local_data_bytes", "APB4_APU__LOCAL_DATA_BYTES"),
|
||||||
|
("local_kws_bytes", "APB4_APU__LOCAL_KWS_BYTES"),
|
||||||
|
):
|
||||||
|
value = re.search(rf"`define\s+{macro}\s+(\d+)", define_rtl)
|
||||||
|
if not value:
|
||||||
|
raise SystemExit(f"{macro} not found in {APU_DEFINE_RTL}")
|
||||||
|
constants[key] = int(value.group(1))
|
||||||
|
return constants
|
||||||
|
|
||||||
|
|
||||||
|
def parse_worst_paths(path: Path, limit: int = 5) -> list[dict[str, Any]]:
|
||||||
|
text = read_text(path)
|
||||||
|
paths: list[dict[str, Any]] = []
|
||||||
|
for block in WORST_BLOCK_RE.split(text):
|
||||||
|
if not block.startswith("Startpoint: "):
|
||||||
|
continue
|
||||||
|
startpoint = re.search(r"^Startpoint: (\S+)", block, re.MULTILINE)
|
||||||
|
endpoint = re.search(r"^Endpoint: (\S+)", block, re.MULTILINE)
|
||||||
|
group = re.search(r"^Path Group: (\S+)", block, re.MULTILINE)
|
||||||
|
path_type = re.search(r"^Path Type: (\S+)", block, re.MULTILINE)
|
||||||
|
slack = re.search(r"(-?[0-9.eE+]+)\s+slack \((MET|VIOLATED)\)", block)
|
||||||
|
if not (startpoint and endpoint and slack):
|
||||||
|
continue
|
||||||
|
paths.append(
|
||||||
|
{
|
||||||
|
"startpoint": startpoint.group(1),
|
||||||
|
"endpoint": endpoint.group(1),
|
||||||
|
"path_group": group.group(1) if group else "",
|
||||||
|
"path_type": path_type.group(1) if path_type else "",
|
||||||
|
"slack_ns": float(slack.group(1)),
|
||||||
|
"verdict": slack.group(2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
kept: list[dict[str, Any]] = []
|
||||||
|
for path_type in ("max", "min"):
|
||||||
|
typed = [item for item in paths if item["path_type"] == path_type]
|
||||||
|
kept.extend(typed[:limit])
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def scan_link_log(path: Path) -> list[str]:
|
||||||
|
if not path.is_file():
|
||||||
|
return []
|
||||||
|
findings = []
|
||||||
|
for line in read_text(path).splitlines():
|
||||||
|
if re.search(r"(?i)not found|blackbox|black box|undefined module", line):
|
||||||
|
findings.append(line.strip())
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def parse_synth(root: Path) -> dict[str, Any]:
|
||||||
|
out_dir = root / "out"
|
||||||
|
rpt_dir = root / "rpt"
|
||||||
|
netlist = out_dir / f"{TOP_MODULE}_yosys.v"
|
||||||
|
area_json = read_json(rpt_dir / f"{TOP_MODULE}_area.json")
|
||||||
|
netlist_text = read_text(netlist)
|
||||||
|
defined_modules = {match.lstrip("\\") for match in MODULE_DEF_RE.findall(netlist_text)}
|
||||||
|
design = area_json["design"]
|
||||||
|
tree, module_cell_types = build_tree(area_json, defined_modules)
|
||||||
|
|
||||||
|
all_types = set(module_cell_types) | set(design.get("num_cells_by_type", {}))
|
||||||
|
blackboxes = {"pdk_macro": [], "pdk_stdcell": 0, "internal_unmapped": [], "unresolved_non_pdk": []}
|
||||||
|
for cell_type in sorted(all_types):
|
||||||
|
klass = classify_cell_type(cell_type, defined_modules)
|
||||||
|
if klass == "pdk_macro":
|
||||||
|
blackboxes["pdk_macro"].append(cell_type)
|
||||||
|
elif klass == "pdk_stdcell":
|
||||||
|
blackboxes["pdk_stdcell"] += 1
|
||||||
|
elif klass == "internal_unmapped":
|
||||||
|
blackboxes["internal_unmapped"].append(cell_type)
|
||||||
|
elif klass == "unresolved_non_pdk":
|
||||||
|
blackboxes["unresolved_non_pdk"].append(cell_type)
|
||||||
|
|
||||||
|
latch_cells = sum(
|
||||||
|
int(count)
|
||||||
|
for cell_type, count in design.get("num_cells_by_type", {}).items()
|
||||||
|
if "DLATCH" in cell_type
|
||||||
|
)
|
||||||
|
clock_gate_cells = sum(
|
||||||
|
int(count)
|
||||||
|
for cell_type, count in design.get("num_cells_by_type", {}).items()
|
||||||
|
if CLOCK_GATE_RE.match(cell_type)
|
||||||
|
)
|
||||||
|
latch_report = rpt_dir / f"{TOP_MODULE}_latches.rpt"
|
||||||
|
latch_select = count_report_lines(latch_report)
|
||||||
|
|
||||||
|
macros = parse_macros(rpt_dir / f"{TOP_MODULE}_macros.rpt")
|
||||||
|
macro_types: dict[str, int] = {}
|
||||||
|
wrapper_area = 0.0
|
||||||
|
for raw_name, stats in area_json["modules"].items():
|
||||||
|
module_type, _ = split_module_name(raw_name)
|
||||||
|
if module_type.startswith(SRAM_WRAPPER_PREFIX):
|
||||||
|
wrapper_area += float(stats.get("area", 0.0))
|
||||||
|
for cell_type, count in stats.get("num_cells_by_type", {}).items():
|
||||||
|
if cell_type.startswith(MACRO_TYPE_PREFIX):
|
||||||
|
macro_types[cell_type] = macro_types.get(cell_type, 0) + int(count)
|
||||||
|
macros["by_type"] = macro_types
|
||||||
|
|
||||||
|
config = parse_config(out_dir / f"{TOP_MODULE}_yosys.config")
|
||||||
|
result = read_json(root / "result-synth.json")
|
||||||
|
return {
|
||||||
|
"root": root,
|
||||||
|
"config": config,
|
||||||
|
"result": result,
|
||||||
|
"design": design,
|
||||||
|
"tree": tree,
|
||||||
|
"macros": macros,
|
||||||
|
"wrapper_area": wrapper_area,
|
||||||
|
"latches": {"mapped_dlatch_cells": latch_cells, "select_report_entries": latch_select},
|
||||||
|
"clock_gate_cells": clock_gate_cells,
|
||||||
|
"blackboxes": blackboxes,
|
||||||
|
"artifacts": {
|
||||||
|
"netlist": netlist,
|
||||||
|
"area_json": rpt_dir / f"{TOP_MODULE}_area.json",
|
||||||
|
"latches": latch_report,
|
||||||
|
"macros": rpt_dir / f"{TOP_MODULE}_macros.rpt",
|
||||||
|
"check": rpt_dir / f"{TOP_MODULE}_synth.rpt",
|
||||||
|
"config": out_dir / f"{TOP_MODULE}_yosys.config",
|
||||||
|
"result": root / "result-synth.json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sta(root: Path) -> dict[str, Any]:
|
||||||
|
metrics = parse_metrics(root / "timing_metrics.rpt")
|
||||||
|
check = parse_unconstrained(root / "unconstrained.rpt")
|
||||||
|
result = read_json(root / "result-sta.json")
|
||||||
|
worst = parse_worst_paths(root / "apu_block_worst.log")
|
||||||
|
worst_max = next((item["slack_ns"] for item in worst if item["path_type"] == "max"), None)
|
||||||
|
worst_min = next((item["slack_ns"] for item in worst if item["path_type"] == "min"), None)
|
||||||
|
return {
|
||||||
|
"root": root,
|
||||||
|
"metrics": metrics,
|
||||||
|
"check": check,
|
||||||
|
"worst_slack_max_ns": worst_max,
|
||||||
|
"worst_slack_min_ns": worst_min,
|
||||||
|
"drv_violations": count_violations(root / "check_types.rpt"),
|
||||||
|
"result": result,
|
||||||
|
"worst_paths": worst,
|
||||||
|
"link_findings": scan_link_log(root / "opensta.log"),
|
||||||
|
"artifacts": {
|
||||||
|
"max_report": root / "apu_block_sta_max.log",
|
||||||
|
"min_report": root / "apu_block_sta_min.log",
|
||||||
|
"worst": root / "apu_block_worst.log",
|
||||||
|
"unconstrained": root / "unconstrained.rpt",
|
||||||
|
"check_types": root / "check_types.rpt",
|
||||||
|
"metrics": root / "timing_metrics.rpt",
|
||||||
|
"result": root / "result-sta.json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_tree(root: Node) -> dict[str, Node]:
|
||||||
|
nodes: dict[str, Node] = {}
|
||||||
|
|
||||||
|
def visit(node: Node) -> None:
|
||||||
|
if node.path:
|
||||||
|
nodes[node.path] = node
|
||||||
|
for child in node.children.values():
|
||||||
|
visit(child)
|
||||||
|
|
||||||
|
for child in root.children.values():
|
||||||
|
visit(child)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def block_table(synths: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
trees = {name: flatten_tree(synth["tree"]) for name, synth in synths.items()}
|
||||||
|
paths: set[str] = set()
|
||||||
|
for nodes in trees.values():
|
||||||
|
paths.update(nodes)
|
||||||
|
rows = []
|
||||||
|
for path in sorted(paths, key=lambda item: (item.count("."), item)):
|
||||||
|
segments = path.split(".")
|
||||||
|
if any(segment.startswith("gen_") for segment in segments):
|
||||||
|
continue
|
||||||
|
row: dict[str, Any] = {"path": path, "depth": path.count(".") + 1}
|
||||||
|
for name, nodes in trees.items():
|
||||||
|
node = nodes.get(path)
|
||||||
|
if node is None:
|
||||||
|
row[name] = {"cells": 0, "macros": 0, "area": 0.0, "present": False}
|
||||||
|
continue
|
||||||
|
cells, macros, area = node.subtree()
|
||||||
|
row[name] = {
|
||||||
|
"cells": cells,
|
||||||
|
"macros": macros,
|
||||||
|
"area": area,
|
||||||
|
"present": node.module_type != "<missing>",
|
||||||
|
"module_type": node.module_type,
|
||||||
|
}
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_configuration(
|
||||||
|
name: str, enable_p7: int, synth: dict[str, Any], sta: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
design = synth["design"]
|
||||||
|
metrics = sta["metrics"]
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"apu_enable_p7": enable_p7,
|
||||||
|
"synth_status": synth["result"].get("status"),
|
||||||
|
"synth_duration_seconds": synth["result"].get("duration_seconds"),
|
||||||
|
"sta_status": sta["result"].get("status"),
|
||||||
|
"sta_duration_seconds": sta["result"].get("duration_seconds"),
|
||||||
|
"num_cells_yosys": int(design.get("num_cells", 0)),
|
||||||
|
"area_um2": float(design.get("area", 0.0)),
|
||||||
|
"sequential_area_um2": float(design.get("sequential_area", 0.0)),
|
||||||
|
"sram_wrapper_area_um2": synth["wrapper_area"],
|
||||||
|
"std_cell_area_um2": float(design.get("area", 0.0)) - synth["wrapper_area"],
|
||||||
|
"macros": synth["macros"],
|
||||||
|
"latches": synth["latches"],
|
||||||
|
"clock_gate_cells": synth["clock_gate_cells"],
|
||||||
|
"blackboxes": synth["blackboxes"],
|
||||||
|
"sta": {
|
||||||
|
"wns_max_ns": metrics["wns_max"],
|
||||||
|
"tns_max_ns": metrics["tns_max"],
|
||||||
|
"wns_min_ns": metrics["wns_min"],
|
||||||
|
"tns_min_ns": metrics["tns_min"],
|
||||||
|
"worst_slack_max_ns": sta["worst_slack_max_ns"],
|
||||||
|
"worst_slack_min_ns": sta["worst_slack_min_ns"],
|
||||||
|
"unconstrained_path_count": sta["check"]["unconstrained_path_count"],
|
||||||
|
"unconstrained_endpoints": sta["check"]["unconstrained_endpoints"],
|
||||||
|
"drv_violations": sta["drv_violations"],
|
||||||
|
"worst_paths": sta["worst_paths"],
|
||||||
|
"link_findings": sta["link_findings"],
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
key: str(value) for key, value in {**synth["artifacts"], **sta["artifacts"]}.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def storage_accounting(synths: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
constants = parse_rtl_constants()
|
||||||
|
wrapper_kib = 1024 * 32 // 8 // 1024 # tc_sram_1024x32 geometry: 4 KiB
|
||||||
|
control_kib = constants["control_store_depth"] * 64 // 8 // 1024
|
||||||
|
data_kib = constants["local_data_logical_banks"] * wrapper_kib
|
||||||
|
kws_window_kib = constants["local_kws_bytes"] // 1024
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"wrapper_geometry": "tc_sram_1024x32",
|
||||||
|
"wrapper_kib": wrapper_kib,
|
||||||
|
"control_store": {
|
||||||
|
"depth_words": constants["control_store_depth"],
|
||||||
|
"wrappers": control_kib // wrapper_kib,
|
||||||
|
"kib": control_kib,
|
||||||
|
},
|
||||||
|
"local_data": {
|
||||||
|
"logical_banks": constants["local_data_logical_banks"],
|
||||||
|
"kib": data_kib,
|
||||||
|
"kws_window_kib": kws_window_kib,
|
||||||
|
"macro_backed_kib": data_kib - kws_window_kib,
|
||||||
|
},
|
||||||
|
"combined_logical": {
|
||||||
|
"wrappers": control_kib // wrapper_kib + constants["local_data_logical_banks"],
|
||||||
|
"kib": control_kib + data_kib,
|
||||||
|
},
|
||||||
|
"rtl_artifacts": [str(LOCAL_SRAM_RTL), str(APU_TOP_RTL), str(APU_DEFINE_RTL)],
|
||||||
|
"per_configuration": {},
|
||||||
|
}
|
||||||
|
for name, synth in synths.items():
|
||||||
|
macros = synth["macros"]
|
||||||
|
result["per_configuration"][name] = {
|
||||||
|
"physical_macro_instances": macros["total_physical"],
|
||||||
|
"by_class": macros["by_class"],
|
||||||
|
"by_type": macros["by_type"],
|
||||||
|
"unclassified": macros["by_class"].get("unclassified", 0),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_slack(value: float | None) -> str:
|
||||||
|
return "n/a" if value is None else f"{value:.3f}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(report: dict[str, Any]) -> str:
|
||||||
|
configs = report["configurations"]
|
||||||
|
storage = report["storage_accounting"]
|
||||||
|
lines = [
|
||||||
|
"# APU block-level IHP130 synthesis + STA evidence",
|
||||||
|
"",
|
||||||
|
f"- recipe: `{report['recipe']}` (locked Yosys/OpenSTA, PDK IHP130)",
|
||||||
|
f"- top: `{TOP_MODULE}`; PCLK target period: {report['target_period_ns']} ns (48 MHz)",
|
||||||
|
f"- baseline: EnableP7=0 (`{configs['baseline']['synth_root_label']}`); "
|
||||||
|
f"expanded: EnableP7=1 (`{configs['expanded']['synth_root_label']}`)",
|
||||||
|
"- all values parsed from the named artifacts listed at the end; report-only,",
|
||||||
|
" no area/power ceiling is evaluated; pre-layout STA is not post-layout signoff.",
|
||||||
|
"",
|
||||||
|
"## Flow status",
|
||||||
|
"",
|
||||||
|
"| configuration | synth | synth duration (s) | STA | STA duration (s) |",
|
||||||
|
"| --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
cfg = configs[name]
|
||||||
|
lines.append(
|
||||||
|
f"| {name} (EnableP7={cfg['apu_enable_p7']}) | {cfg['synth_status']} | "
|
||||||
|
f"{cfg['synth_duration_seconds']:.1f} | {cfg['sta_status']} | "
|
||||||
|
f"{cfg['sta_duration_seconds']:.1f} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"## Static timing (PCLK 20.833 ns target; hold reported separately)",
|
||||||
|
"",
|
||||||
|
"WNS/TNS below are the SoC-flow-compatible OpenSTA metrics (clipped at 0",
|
||||||
|
"when no negative slack exists); the true worst slack comes from the",
|
||||||
|
"worst-paths report.",
|
||||||
|
"",
|
||||||
|
"| configuration | WNS max (ns) | TNS max (ns) | worst max slack (ns) | WNS min/hold (ns) | TNS min/hold (ns) | worst min slack (ns) | unconstrained paths |",
|
||||||
|
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
sta = configs[name]["sta"]
|
||||||
|
lines.append(
|
||||||
|
f"| {name} | {sta['wns_max_ns']:.3f} | {sta['tns_max_ns']:.3f} | "
|
||||||
|
f"{fmt_slack(sta['worst_slack_max_ns'])} | {sta['wns_min_ns']:.3f} | "
|
||||||
|
f"{sta['tns_min_ns']:.3f} | {fmt_slack(sta['worst_slack_min_ns'])} | "
|
||||||
|
f"{sta['unconstrained_path_count']} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"Acceptance reference (docs/ip/apu.md): max-delay WNS >= 0 and TNS = 0, no",
|
||||||
|
"unconstrained active APU path; hold (min) analysis reported separately above.",
|
||||||
|
"",
|
||||||
|
"## Integrity checks",
|
||||||
|
"",
|
||||||
|
"| configuration | inferred latches | clock-gate cells | unresolved non-PDK black boxes | PDK macro black boxes | unmapped internal cells | STA DRV violations |",
|
||||||
|
"| --- | --- | --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
cfg = configs[name]
|
||||||
|
lines.append(
|
||||||
|
f"| {name} | {cfg['latches']['mapped_dlatch_cells']} | "
|
||||||
|
f"{cfg['clock_gate_cells']} | "
|
||||||
|
f"{len(cfg['blackboxes']['unresolved_non_pdk'])} | "
|
||||||
|
f"{', '.join(cfg['blackboxes']['pdk_macro']) or 'none'} | "
|
||||||
|
f"{len(cfg['blackboxes']['internal_unmapped'])} | "
|
||||||
|
f"{cfg['sta']['drv_violations']} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"PDK macro cells are Liberty black boxes exactly as in the whole-SoC flow",
|
||||||
|
"(`read_liberty -lib`); their timing views come from the sg13g2_sram slow",
|
||||||
|
"Liberty set during STA. Standard cells resolve against sg13g2_stdcell.",
|
||||||
|
"",
|
||||||
|
"## SRAM macro accounting",
|
||||||
|
"",
|
||||||
|
f"- wrapper geometry: `{storage['wrapper_geometry']}` = {storage['wrapper_kib']} KiB",
|
||||||
|
f"- control store: depth {storage['control_store']['depth_words']} words -> "
|
||||||
|
f"{storage['control_store']['wrappers']} wrappers, {storage['control_store']['kib']} KiB",
|
||||||
|
f"- local data: {storage['local_data']['logical_banks']} logical banks -> "
|
||||||
|
f"{storage['local_data']['kib']} KiB "
|
||||||
|
f"(macro-backed {storage['local_data']['macro_backed_kib']} KiB + "
|
||||||
|
f"KWS register-file window {storage['local_data']['kws_window_kib']} KiB)",
|
||||||
|
f"- combined logical: {storage['combined_logical']['wrappers']} wrappers, "
|
||||||
|
f"{storage['combined_logical']['kib']} KiB",
|
||||||
|
"",
|
||||||
|
"| configuration | control-store macros | local-data macros | loader path-stack macros | total physical macros |",
|
||||||
|
"| --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
per = storage["per_configuration"][name]
|
||||||
|
lines.append(
|
||||||
|
f"| {name} | {per['by_class'].get('control_store', 0)} | "
|
||||||
|
f"{per['by_class'].get('local_data', 0)} | "
|
||||||
|
f"{per['by_class'].get('loader_path_stack', 0)} | "
|
||||||
|
f"{per['physical_macro_instances']} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
f"Capacity reference ({DOC_REFERENCE}): 8 control-store + 28 local-data = 36 logical",
|
||||||
|
"wrappers (144 KiB combined) vs the pre-expansion P5 baseline of 4 + 28 = 32",
|
||||||
|
"(128 KiB). The loader path-stack macros above are the verifier workspace and",
|
||||||
|
"are reported separately as required. Note: on the current RTL the control-store",
|
||||||
|
"Depth is fixed at 4096 for both EnableP7 values, so both configurations measure",
|
||||||
|
"36 logical wrappers; the 32-wrapper P5 baseline is the pre-expansion RTL, not",
|
||||||
|
"EnableP7=0 of the current RTL.",
|
||||||
|
"",
|
||||||
|
"## Per-block cell/macro/area accounting (hierarchy subtree totals)",
|
||||||
|
"",
|
||||||
|
"| block | cells P7=0 | cells P7=1 | delta | area um2 P7=0 | area um2 P7=1 | delta | macros P7=0/P7=1 |",
|
||||||
|
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||||
|
]
|
||||||
|
for row in report["blocks"]:
|
||||||
|
base = row["baseline"]
|
||||||
|
exp = row["expanded"]
|
||||||
|
indent = " " * (row["depth"] - 1)
|
||||||
|
lines.append(
|
||||||
|
f"| {indent}`{row['path']}` | {base['cells']} | {exp['cells']} | "
|
||||||
|
f"{exp['cells'] - base['cells']:+d} | {base['area']:.1f} | {exp['area']:.1f} | "
|
||||||
|
f"{exp['area'] - base['area']:+.1f} | {base['macros']}/{exp['macros']} |"
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"Cells are leaf standard cells plus macro cells inside the kept hierarchy;",
|
||||||
|
"areas are the sums of the Liberty cell areas inside each subtree. The",
|
||||||
|
"KWS register-file window lives in `u_kws_sram_client` flops, not in",
|
||||||
|
"SRAM macros, so it appears as standard-cell area above.",
|
||||||
|
"",
|
||||||
|
"## Worst max-delay paths (top 5 per configuration)",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
lines.append(f"### {name}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| startpoint | endpoint | slack (ns) | verdict |")
|
||||||
|
lines.append("| --- | --- | --- | --- |")
|
||||||
|
for path in configs[name]["sta"]["worst_paths"]:
|
||||||
|
if path["path_type"] != "max":
|
||||||
|
continue
|
||||||
|
lines.append(
|
||||||
|
f"| `{path['startpoint']}` | `{path['endpoint']}` | "
|
||||||
|
f"{path['slack_ns']:.3f} | {path['verdict']} |"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
lines += [
|
||||||
|
"## Artifacts",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for name in ("baseline", "expanded"):
|
||||||
|
lines.append(f"- {name}:")
|
||||||
|
for key, value in configs[name]["artifacts"].items():
|
||||||
|
lines.append(f" - {key}: `{value}`")
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"Assumptions are documented in physical/smoke/sta/opensta/apu_block.sdc",
|
||||||
|
"(single PCLK domain, 30%/30% I/O budget, false path from rst_n_i).",
|
||||||
|
]
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--baseline-synth", type=Path, required=True)
|
||||||
|
parser.add_argument("--expanded-synth", type=Path, required=True)
|
||||||
|
parser.add_argument("--baseline-sta", type=Path, required=True)
|
||||||
|
parser.add_argument("--expanded-sta", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--recipe", default="balanced", choices=("balanced", "area", "speed"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--top",
|
||||||
|
default="apb4_apu",
|
||||||
|
help="netlist top module; apb4_apu for the APU evidence flow",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
global TOP_MODULE
|
||||||
|
args = parse_args()
|
||||||
|
TOP_MODULE = args.top
|
||||||
|
synths = {
|
||||||
|
"baseline": parse_synth(args.baseline_synth),
|
||||||
|
"expanded": parse_synth(args.expanded_synth),
|
||||||
|
}
|
||||||
|
stas = {
|
||||||
|
"baseline": parse_sta(args.baseline_sta),
|
||||||
|
"expanded": parse_sta(args.expanded_sta),
|
||||||
|
}
|
||||||
|
for name, synth in synths.items():
|
||||||
|
expected = {"baseline": "0", "expanded": "1"}[name]
|
||||||
|
observed = synth["config"].get("APU_ENABLE_P7")
|
||||||
|
if observed != expected:
|
||||||
|
raise SystemExit(
|
||||||
|
f"{name} synth config APU_ENABLE_P7={observed}, expected {expected}: "
|
||||||
|
f"{args.baseline_synth if name == 'baseline' else args.expanded_synth}"
|
||||||
|
)
|
||||||
|
period_ps = float(synths["baseline"]["config"].get("PERIOD_PS", "0"))
|
||||||
|
|
||||||
|
report: dict[str, Any] = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"flow": "apu-block-ihp130",
|
||||||
|
"generated_by": "physical/smoke/syn/yosys/report_apu_block.py",
|
||||||
|
"recipe": args.recipe,
|
||||||
|
"target_period_ns": period_ps / 1000.0,
|
||||||
|
"configurations": {
|
||||||
|
name: summarize_configuration(name, int(p7), synths[name], stas[name])
|
||||||
|
for name, p7 in (("baseline", "0"), ("expanded", "1"))
|
||||||
|
},
|
||||||
|
"storage_accounting": storage_accounting(synths),
|
||||||
|
"documentation_reference": {
|
||||||
|
"source": DOC_REFERENCE,
|
||||||
|
"expanded_logical_wrappers": 36,
|
||||||
|
"baseline_logical_wrappers": 32,
|
||||||
|
"expanded_combined_kib": 144,
|
||||||
|
"baseline_combined_kib": 128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
report["blocks"] = block_table(synths)
|
||||||
|
for name, cfg in report["configurations"].items():
|
||||||
|
cfg["synth_root_label"] = str(synths[name]["root"])
|
||||||
|
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
json_path = args.output_dir / "apu-block-synth.json"
|
||||||
|
md_path = args.output_dir / "apu-block-synth.md"
|
||||||
|
json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||||
|
md_path.write_text(render_markdown(report), encoding="utf-8")
|
||||||
|
print(f"generated: {json_path}")
|
||||||
|
print(f"generated: {md_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Copyright (c) 2022 ETH Zurich and University of Bologna.
|
||||||
|
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# -- Adaptable modifications are redistributed under compatible License --
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023-2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# APU block-level variant of abc_balanced.script (same recipe minus one pass).
|
||||||
|
#
|
||||||
|
# Deviation from the whole-SoC recipe: the "&fraig -x" SAT-sweeping pass is
|
||||||
|
# removed. On the APU multiplier-dominated AIGs (apu_kernel_engine and the
|
||||||
|
# reconstruction/transform/resampler engines) "&fraig -x" does not terminate:
|
||||||
|
# measured 14+ hours at 100% CPU on the 163k-AND apu_kernel_engine network
|
||||||
|
# (yosys-abc 0.67, PDK_IHP130, 2026-09-16/17), with larger modules still
|
||||||
|
# pending behind it. With only that pass removed the same module maps in
|
||||||
|
# ~19 s and every other pass, the &nf delay target, and the tool versions are
|
||||||
|
# identical to the whole-SoC flow. The SoC recipe itself is untouched.
|
||||||
|
|
||||||
|
strash
|
||||||
|
&get -n
|
||||||
|
&put
|
||||||
|
scorr
|
||||||
|
dc2
|
||||||
|
dretime
|
||||||
|
strash
|
||||||
|
&get -n
|
||||||
|
&dch -f
|
||||||
|
&nf {D}
|
||||||
|
&put
|
||||||
|
buffer
|
||||||
|
upsize {D}
|
||||||
|
dnsize {D}
|
||||||
|
stime -p
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Copyright (c) 2022 ETH Zurich and University of Bologna.
|
||||||
|
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Authors:
|
||||||
|
# - Philippe Sauter <phsauter@iis.ee.ethz.ch>
|
||||||
|
#
|
||||||
|
# -- Adaptable modifications are redistributed under compatible License --
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023-2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# KWS-window variant of abc_balanced_apu_block.script, applied ONLY to the
|
||||||
|
# apu_kws_sram_client module by apu_block_synth.tcl (two-phase ABC).
|
||||||
|
#
|
||||||
|
# apu_kws_sram_client models the 64 KiB KWS model/scratch register-file window
|
||||||
|
# as one 16384x32 RTL memory with 36 asynchronous read ports and 7 write
|
||||||
|
# ports; memory_map lowers it to ~12.6M gate-level cells (36.7M ANDs after
|
||||||
|
# strash). On that network "&dch -f" (choice computation) does not terminate,
|
||||||
|
# exactly like the stock recipe's "&fraig -x":
|
||||||
|
# * "&fraig -x": two orphaned yosys-abc children at ~208 GB RSS each, killed
|
||||||
|
# (2026-09-16); 14+ h at 100% CPU on the 163k-AND apu_kernel_engine.
|
||||||
|
# * "&dch -f": >50 min with zero allocation progress on the 35.8M-AND
|
||||||
|
# network, killed (2026-09-17, yosys-abc 0.67).
|
||||||
|
# With both passes removed the same network maps in 1 h 27 min at 20.4 GB peak
|
||||||
|
# RSS (standalone yosys-abc probe on the exact abc input BLIF, -D 20833 ps,
|
||||||
|
# sg13g2_stdcell_typ_1p20V_25C), versus ~98 GB peak abc in the whole-SoC flow.
|
||||||
|
# Every other APU module still receives the standard block recipe; the liberty,
|
||||||
|
# driving cell, load, and delay target are identical in both phases.
|
||||||
|
|
||||||
|
strash
|
||||||
|
&get -n
|
||||||
|
&put
|
||||||
|
scorr
|
||||||
|
dc2
|
||||||
|
dretime
|
||||||
|
strash
|
||||||
|
&get -n
|
||||||
|
&nf {D}
|
||||||
|
&put
|
||||||
|
buffer
|
||||||
|
upsize {D}
|
||||||
|
dnsize {D}
|
||||||
|
stime -p
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# APU block-level synthesis (top: apb4_apu), derived from script/synth.tcl.
|
||||||
|
# Differences from the whole-SoC flow:
|
||||||
|
# * APU_ENABLE_P7 selects the apb4_apu EnableP7 parameter via chparam.
|
||||||
|
# * The hierarchy keep list comes from the flow environment and keeps every
|
||||||
|
# apu_* submodule plus the tc_sram wrappers for per-block accounting.
|
||||||
|
# * Additional latch and PDK macro inventory reports are emitted (before
|
||||||
|
# ABC, so long-running runs still leave that evidence behind).
|
||||||
|
# * The ABC recipe prefers abc_<recipe>_apu_block.script when present: the
|
||||||
|
# whole-SoC recipe's "&fraig -x" pass does not terminate on the APU
|
||||||
|
# multiplier-dominated AIGs; the variant drops only that pass.
|
||||||
|
# * ABC runs in two phases: the giant KWS register-file window module
|
||||||
|
# (apu_kws_sram_client) uses abc_<recipe>_apu_block_kws.script (the block
|
||||||
|
# recipe minus "&dch -f", which does not terminate on its 36.7M-AND
|
||||||
|
# network); every other module uses the standard block recipe.
|
||||||
|
|
||||||
|
# get environment variables
|
||||||
|
set script_dir [file dirname [info script]]
|
||||||
|
source $script_dir/common.tcl
|
||||||
|
|
||||||
|
# read liberty files and prepare some variables
|
||||||
|
source $script_dir/init_tech.tcl
|
||||||
|
|
||||||
|
if {![info exists ::env(APU_ENABLE_P7)] || ($::env(APU_ENABLE_P7) ni {0 1})} {
|
||||||
|
error "APU_ENABLE_P7 must be 0 or 1"
|
||||||
|
}
|
||||||
|
set apu_enable_p7 $::env(APU_ENABLE_P7)
|
||||||
|
|
||||||
|
# # read design
|
||||||
|
# --allow-toplevel-iface-ports: apb4_apu has SystemVerilog interface ports
|
||||||
|
# (apb4/axi4/streams); slang elaborates each as an unconnected top-level
|
||||||
|
# interface instance so the block can be synthesized standalone.
|
||||||
|
# -G EnableP7: slang elaborates parameters itself, so the APU feature
|
||||||
|
# configuration is applied as a top-level parameter override (the same
|
||||||
|
# mechanism as the .EnableP7(...) binding in rtl/mini/top/apb4_periph.sv);
|
||||||
|
# yosys chparam cannot re-parameterize an already-elaborated module.
|
||||||
|
yosys read_slang --top $top_design -F $sv_flist -G EnableP7=$apu_enable_p7 \
|
||||||
|
--keep-hierarchy --allow-use-before-declare --ignore-initial --ignore-timing \
|
||||||
|
--allow-toplevel-iface-ports
|
||||||
|
|
||||||
|
# # preserve hierarchy of selected modules/instances
|
||||||
|
if { [info exists ::env(YOSYS_KEEP_HIER_INST)] } {
|
||||||
|
foreach sel $::env(YOSYS_KEEP_HIER_INST) {
|
||||||
|
puts "Keeping hierarchy of selection: $sel"
|
||||||
|
yosys select -list {*}$sel
|
||||||
|
yosys setattr -set keep_hierarchy 1 {*}$sel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# map dont_touch attribute commonly applied to output-nets of async regs to keep
|
||||||
|
yosys attrmap -rename dont_touch keep
|
||||||
|
# copy the keep attribute to their driving cells (retain on net for debugging)
|
||||||
|
yosys attrmvcp -copy -attr keep
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# this section heavily borrows from the yosys synth command:
|
||||||
|
# synth - check
|
||||||
|
yosys hierarchy -check -top $top_design
|
||||||
|
yosys proc
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_initial.rpt" stat
|
||||||
|
yosys write_verilog -norename -noexpr -attr2comment ${build_dir}/${proj_name}_yosys_initial.v
|
||||||
|
|
||||||
|
# synth - coarse:
|
||||||
|
# yosys synth -run coarse -noalumacc
|
||||||
|
yosys opt_expr
|
||||||
|
yosys opt_clean
|
||||||
|
yosys check
|
||||||
|
yosys opt -noff
|
||||||
|
yosys fsm
|
||||||
|
yosys opt
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_initial_opt.rpt" stat
|
||||||
|
yosys wreduce
|
||||||
|
yosys peepopt
|
||||||
|
yosys opt_clean
|
||||||
|
yosys opt -full
|
||||||
|
yosys booth
|
||||||
|
yosys alumacc
|
||||||
|
yosys share
|
||||||
|
yosys opt
|
||||||
|
yosys memory
|
||||||
|
yosys opt -fast
|
||||||
|
|
||||||
|
yosys opt_dff -sat -nodffe -nosdff
|
||||||
|
yosys share
|
||||||
|
yosys opt -full
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
yosys write_verilog -norename ${work_dir}/${proj_name}_abstract.yosys.v
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_abstract.rpt" stat -tech cmos
|
||||||
|
|
||||||
|
yosys techmap
|
||||||
|
yosys opt -fast
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_generic.rpt" stat -tech cmos
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_generic.json" stat -json -tech cmos
|
||||||
|
|
||||||
|
if {[envVarValid "YOSYS_FLATTEN_HIER"]} {
|
||||||
|
yosys flatten
|
||||||
|
}
|
||||||
|
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# split internal nets
|
||||||
|
yosys splitnets -format __v
|
||||||
|
# rename DFFs from the driven signal
|
||||||
|
yosys rename -wire -suffix _reg t:*DFF*
|
||||||
|
yosys select -write ${report_dir}/${proj_name}_registers.rpt t:*DFF*
|
||||||
|
# rename all other cells
|
||||||
|
# yosys autoname t:*DFF* %n
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_pre_tech.rpt" stat -tech cmos
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_pre_tech.json" stat -json -tech cmos
|
||||||
|
|
||||||
|
# inferred-latch and PDK macro inventories are mapping-independent; emit them
|
||||||
|
# before ABC so long-running runs still leave this evidence behind
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_latches.rpt" select -list t:*DLATCH*
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_macros.rpt" select -list t:RM_IHPSG13*
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# mapping to technology
|
||||||
|
|
||||||
|
# Prefer the APU block recipe variant (abc_<recipe>_apu_block.script): the
|
||||||
|
# whole-SoC recipe's "&fraig -x" SAT sweeping does not terminate on the APU
|
||||||
|
# multiplier-dominated AIGs (14+ h on apu_kernel_engine alone); the variant
|
||||||
|
# drops only that pass. See the script header for the measured evidence.
|
||||||
|
set abc_script_block $script_dir/abc_${synth_recipe}_apu_block.script
|
||||||
|
set abc_script_src $script_dir/abc_${synth_recipe}.script
|
||||||
|
if {[file exists $abc_script_block]} {
|
||||||
|
set abc_script_src $abc_script_block
|
||||||
|
} elseif {![file exists $abc_script_src]} {
|
||||||
|
error "unsupported SYNTH_RECIPE: $synth_recipe"
|
||||||
|
}
|
||||||
|
set abc_script [processAbcScript $abc_script_src]
|
||||||
|
|
||||||
|
set abc_constr $work_dir/abc.constr
|
||||||
|
set constr_file [open $abc_constr w]
|
||||||
|
puts $constr_file "set_driving_cell $abc_driver"
|
||||||
|
puts $constr_file "set_load $abc_load"
|
||||||
|
close $constr_file
|
||||||
|
|
||||||
|
puts "Using SYNTH_RECIPE=$synth_recipe ABC script $abc_script_src (period_ps=$period_ps)"
|
||||||
|
yosys dfflibmap {*}$tech_cells_args
|
||||||
|
|
||||||
|
# Two-phase ABC. apu_kws_sram_client models the 64 KiB KWS model/scratch
|
||||||
|
# register-file window as one 16384x32 RTL memory with 36 asynchronous read
|
||||||
|
# ports and 7 write ports; memory_map lowers it to ~12.6M gate-level cells
|
||||||
|
# (36.7M ANDs after strash). On that network the block recipe's "&dch -f"
|
||||||
|
# choice pass does not terminate (>50 min without progress, killed,
|
||||||
|
# yosys-abc 0.67, 2026-09-17), exactly like the stock recipe's "&fraig -x"
|
||||||
|
# (two orphaned abc children at ~208 GB RSS each, killed, 2026-09-16). The
|
||||||
|
# KWS module alone is therefore mapped with abc_<recipe>_apu_block_kws.script
|
||||||
|
# (block recipe minus both passes; measured 1 h 27 min / 20.4 GB peak on the
|
||||||
|
# exact abc input network, -D 20833 ps, sg13g2 typ liberty). Every other
|
||||||
|
# module keeps the standard block recipe with identical liberty, driving
|
||||||
|
# cell, load, and delay target. The assert fails the run loudly if RTL
|
||||||
|
# renames the module instead of silently hanging in the heavy recipe.
|
||||||
|
set abc_script_kws $script_dir/abc_${synth_recipe}_apu_block_kws.script
|
||||||
|
if {![file exists $abc_script_kws]} {
|
||||||
|
error "missing KWS-window ABC script: $abc_script_kws"
|
||||||
|
}
|
||||||
|
set abc_kws [processAbcScript $abc_script_kws]
|
||||||
|
puts "Using KWS-window ABC script $abc_script_kws for apu_kws_sram_client only"
|
||||||
|
yosys select -assert-any apu_kws_sram_client*
|
||||||
|
yosys abc {*}$tech_cells_args -D $period_ps -script $abc_kws -constr $abc_constr apu_kws_sram_client*
|
||||||
|
yosys abc {*}$tech_cells_args -D $period_ps -script $abc_script -constr $abc_constr * apu_kws_sram_client* %d
|
||||||
|
|
||||||
|
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# prep for openROAD
|
||||||
|
yosys write_verilog -norename -noexpr -attr2comment ${build_dir}/${proj_name}_yosys_debug.v
|
||||||
|
|
||||||
|
yosys splitnets -ports -format __v
|
||||||
|
yosys setundef -zero
|
||||||
|
yosys clean -purge
|
||||||
|
|
||||||
|
if {[llength $tech_cell_tiehi] > 0 && [llength $tech_cell_tielo] > 0} {
|
||||||
|
yosys hilomap -singleton -hicell {*}$tech_cell_tiehi -locell {*}$tech_cell_tielo
|
||||||
|
}
|
||||||
|
|
||||||
|
# final reports
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_synth.rpt" check
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_area.rpt" stat -top $top_design {*}$liberty_args
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_area.json" stat -json -top $top_design {*}$liberty_args
|
||||||
|
yosys tee -q -o "${report_dir}/${proj_name}_area_logic.rpt" stat -top $top_design {*}$tech_cells_args
|
||||||
|
|
||||||
|
# final netlist
|
||||||
|
yosys write_verilog -noattr -noexpr -nohex -nodec $netlist
|
||||||
|
|
||||||
|
# Record the configuration only after the netlist has been written successfully.
|
||||||
|
set config_tmp "${config}.tmp"
|
||||||
|
set config_file [open $config_tmp "w"]
|
||||||
|
puts $config_file "PDK=$pdk"
|
||||||
|
puts $config_file "SOC=$soc"
|
||||||
|
puts $config_file "TOP_DESIGN=$top_design"
|
||||||
|
puts $config_file "SYNTH_RECIPE=$synth_recipe"
|
||||||
|
puts $config_file "PERIOD_PS=$period_ps"
|
||||||
|
puts $config_file "APU_ENABLE_P7=$apu_enable_p7"
|
||||||
|
puts $config_file "HAVE_SRAM_MACRO=$::env(HAVE_SRAM_MACRO)"
|
||||||
|
puts $config_file "SRAM_SIZE_KIB=$::env(SRAM_SIZE_KIB)"
|
||||||
|
close $config_file
|
||||||
|
file rename -force $config_tmp $config
|
||||||
@@ -1713,7 +1713,7 @@
|
|||||||
"function_sections": [],
|
"function_sections": [],
|
||||||
"software_sections": [],
|
"software_sections": [],
|
||||||
"notes": [
|
"notes": [
|
||||||
"The current capability word is 0x000001BD: WAV, FLAC, private DMA, ring, stream, sequencer and resampler bits are set. MP3 and KWS bits remain clear. Capability is not evidence that every production codec corpus or physical release gate is complete.",
|
"The default product capability word remains 0x000001BD until every P7/P8 release gate passes. The dedicated P7 evidence profile uses 0x000001FD for WAV, FLAC, private DMA, ring, stream, KWS, sequencer and resampler; MP3 remains clear with deferred P6.",
|
||||||
"LP loads a bounded APUMC image before submitting work. Expanded P5 RTL has 4096 64-bit control-store words (32 KiB), CAPABILITY1=0x01827020 and APB version 1.1 while retaining APUMC V1 image compatibility.",
|
"LP loads a bounded APUMC image before submitting work. Expanded P5 RTL has 4096 64-bit control-store words (32 KiB), CAPABILITY1=0x01827020 and APB version 1.1 while retaining APUMC V1 image compatibility.",
|
||||||
"The loader checks header, ABI, bounds, entry descriptors and CRC before publishing valid/lock state. Rejected or aborted loads must leave stale instructions unexecutable.",
|
"The loader checks header, ABI, bounds, entry descriptors and CRC before publishing valid/lock state. Rejected or aborted loads must leave stale instructions unexecutable.",
|
||||||
"Direct jobs use JOB_* staging registers; ring jobs use descriptor ownership and producer/consumer rules. Buffer limits, codec entry bounds, ownership, quiesce and transport completion participate in admission and termination.",
|
"Direct jobs use JOB_* staging registers; ring jobs use descriptor ownership and producer/consumer rules. Buffer limits, codec entry bounds, ownership, quiesce and transport completion participate in admission and termination.",
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ module apb4_apu #(
|
|||||||
.clk_i (clk_i),
|
.clk_i (clk_i),
|
||||||
.rst_n_i (rst_n_i),
|
.rst_n_i (rst_n_i),
|
||||||
.abort_i (s_dma_abort),
|
.abort_i (s_dma_abort),
|
||||||
.quiesce_i (s_dma_admission_block && s_mc_idle),
|
.quiesce_i (s_dma_admission_block && s_mc_idle && !s_kws_loader_busy),
|
||||||
.bridge_epoch_i (bridge_epoch_i),
|
.bridge_epoch_i (bridge_epoch_i),
|
||||||
.perf_enable_i (s_perf_enable),
|
.perf_enable_i (s_perf_enable),
|
||||||
.counter_clear_i (s_cnt_clear),
|
.counter_clear_i (s_cnt_clear),
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ module apu_kws_engine (
|
|||||||
logic signed [31:0] s_fft_left_real_data, s_fft_left_imag_data;
|
logic signed [31:0] s_fft_left_real_data, s_fft_left_imag_data;
|
||||||
logic signed [31:0] s_fft_right_real_data, s_fft_right_imag_data;
|
logic signed [31:0] s_fft_right_real_data, s_fft_right_imag_data;
|
||||||
logic signed [31:0] s_log_data;
|
logic signed [31:0] s_log_data;
|
||||||
|
logic [ 7:0] s_dct_output_value;
|
||||||
logic signed [7:0] s_logit_data, s_softmax_data[0:11];
|
logic signed [7:0] s_logit_data, s_softmax_data[0:11];
|
||||||
logic s_fft_overflow;
|
logic s_fft_overflow;
|
||||||
|
|
||||||
@@ -723,6 +724,7 @@ module apu_kws_engine (
|
|||||||
assign s_log_row_sum = {1'b0, s_snapshot_start_q} + {1'b0, s_mfcc_row_q};
|
assign s_log_row_sum = {1'b0, s_snapshot_start_q} + {1'b0, s_mfcc_row_q};
|
||||||
assign s_log_row_index = (s_log_row_sum >= 7'd50) ? 6'(s_log_row_sum - 7'd50) : 6'(s_log_row_sum);
|
assign s_log_row_index = (s_log_row_sum >= 7'd50) ? 6'(s_log_row_sum - 7'd50) : 6'(s_log_row_sum);
|
||||||
assign s_dct_coefficient = rne_shift(s_dct_total, 30);
|
assign s_dct_coefficient = rne_shift(s_dct_total, 30);
|
||||||
|
assign s_dct_output_value = 8'((s_dct_coefficient + 64'sh3087_c475) / 64'sh0095_af17);
|
||||||
|
|
||||||
assign s_weight_base = operator_weight_base(s_operator_q);
|
assign s_weight_base = operator_weight_base(s_operator_q);
|
||||||
assign s_bias_base = operator_bias_base(s_operator_q);
|
assign s_bias_base = operator_bias_base(s_operator_q);
|
||||||
@@ -1092,7 +1094,7 @@ module apu_kws_engine (
|
|||||||
scratch_write_valid_o[0] = 1'b1;
|
scratch_write_valid_o[0] = 1'b1;
|
||||||
scratch_write_addr_o[0] = `RETROSOC_APU_KWS__SCRATCH_MFCC_BASE +
|
scratch_write_addr_o[0] = `RETROSOC_APU_KWS__SCRATCH_MFCC_BASE +
|
||||||
16'(9'(s_mfcc_row_q) * 9'd10 + 9'(s_mfcc_coefficient_q));
|
16'(9'(s_mfcc_row_q) * 9'd10 + 9'(s_mfcc_coefficient_q));
|
||||||
scratch_write_data_o[0] = {4{8'(s_dct_coefficient / 64'sh0095_af17 + 64'sd83)}};
|
scratch_write_data_o[0] = {4{s_dct_output_value}};
|
||||||
scratch_write_strb_o[0] = 4'b0001 << scratch_write_addr_o[0][1:0];
|
scratch_write_strb_o[0] = 4'b0001 << scratch_write_addr_o[0][1:0];
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -111,11 +111,12 @@ module apu_reg #(
|
|||||||
);
|
);
|
||||||
localparam logic [31:0] IpId = 32'h4150_5530;
|
localparam logic [31:0] IpId = 32'h4150_5530;
|
||||||
localparam logic [31:0] IpVersion = (EnableP5 || EnableP7) ? 32'h0001_0001 : 32'h0001_0000;
|
localparam logic [31:0] IpVersion = (EnableP5 || EnableP7) ? 32'h0001_0001 : 32'h0001_0000;
|
||||||
// P7 logic remains deliberately unadvertised until golden-corpus release
|
localparam logic [31:0] P5Capability0 = EnableP5 ? 32'h0000_01bd : 32'h0000_0198;
|
||||||
// qualification; software must fail closed from the established P5 value.
|
// EnableP7 advertises the qualified KWS capability after the frozen release
|
||||||
localparam logic [31:0] Capability0 = EnableP5 ? 32'h0000_01bd : 32'h0000_0198;
|
// gates pass; the MP3 bit 1 stays zero for the deferred P6.
|
||||||
|
localparam logic [31:0] Capability0 = EnableP7 ? 32'h0000_01fd : P5Capability0;
|
||||||
localparam logic [31:0] Capability1 = (EnableP5 || EnableP7) ? 32'h0182_7020 : 32'h0182_7010;
|
localparam logic [31:0] Capability1 = (EnableP5 || EnableP7) ? 32'h0182_7020 : 32'h0182_7010;
|
||||||
localparam logic [31:0] AbiDigest = 32'd0;
|
localparam logic [31:0] AbiDigest = EnableP7 ? 32'hf500_5d7c : 32'd0;
|
||||||
localparam logic [10:0] IrqMask = 11'h7ff;
|
localparam logic [10:0] IrqMask = 11'h7ff;
|
||||||
localparam logic [31:0] TimeoutReset = 32'h0000_ffff;
|
localparam logic [31:0] TimeoutReset = 32'h0000_ffff;
|
||||||
localparam logic [31:0] RingCoalesceReset = 32'h0001_0001;
|
localparam logic [31:0] RingCoalesceReset = 32'h0001_0001;
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ $(SOC_TOPOLOGY_STAMP): $(SOC_TOPOLOGY_SOURCE) $(SOC_TOPOLOGY_SCRIPT) $(MEMORY_MA
|
|||||||
--external-irq-count $(if $(filter PRODUCT,$(MINI_MODE)),62,30)
|
--external-irq-count $(if $(filter PRODUCT,$(MINI_MODE)),62,30)
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
|
$(SOC_IRQ_METADATA): $(SOC_TOPOLOGY_STAMP)
|
||||||
|
@test -f $@ || { echo "missing generated IRQ metadata: $@" >&2; exit 1; }
|
||||||
|
|
||||||
soc-topology: $(SOC_TOPOLOGY_STAMP)
|
soc-topology: $(SOC_TOPOLOGY_STAMP)
|
||||||
|
|
||||||
check-soc-topology:
|
check-soc-topology:
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ endif
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
DEF_VAL += -DAPP_$(APP)
|
DEF_VAL += -DAPP_$(APP)
|
||||||
|
ifeq ($(APU_ENABLE_P7),YES)
|
||||||
|
DEF_VAL += -DRS_APU_RELEASE_P7
|
||||||
|
endif
|
||||||
DEF_VAL += -DCOMPILER_NAME='"$(CC)"'
|
DEF_VAL += -DCOMPILER_NAME='"$(CC)"'
|
||||||
DEF_VAL += -DCOMPILER_CFLAGS='"$(GCC_FLAGS) $(SW_WARN_FLAGS)"'
|
DEF_VAL += -DCOMPILER_CFLAGS='"$(GCC_FLAGS) $(SW_WARN_FLAGS)"'
|
||||||
DEF_VAL += -DCOMPILER_ISA='"$(ISA_FLAGS)"'
|
DEF_VAL += -DCOMPILER_ISA='"$(ISA_FLAGS)"'
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ module retrosoc (
|
|||||||
output logic pclk_idle_o
|
output logic pclk_idle_o
|
||||||
// verilog_format: on
|
// verilog_format: on
|
||||||
);
|
);
|
||||||
|
`ifdef APU_ENABLE_P7
|
||||||
|
localparam bit ApuEnableP7 = 1'b1;
|
||||||
|
`else
|
||||||
|
localparam bit ApuEnableP7 = 1'b0;
|
||||||
|
`endif
|
||||||
|
|
||||||
// verilog_format: off -- preserve reviewed column alignment
|
// verilog_format: off -- preserve reviewed column alignment
|
||||||
// Generated fabric links use the common 32-bit AXI4 contract.
|
// Generated fabric links use the common 32-bit AXI4 contract.
|
||||||
@@ -853,7 +858,9 @@ module retrosoc (
|
|||||||
.perf_opipsram_wait_o (s_perf_opipsram_wait)
|
.perf_opipsram_wait_o (s_perf_opipsram_wait)
|
||||||
);
|
);
|
||||||
|
|
||||||
apb4_periph u_apb4_periph (
|
apb4_periph #(
|
||||||
|
.EnableP7(ApuEnableP7)
|
||||||
|
) u_apb4_periph (
|
||||||
.clk_i (clk_pclk_i),
|
.clk_i (clk_pclk_i),
|
||||||
.rst_n_i (rst_pclk_n_i),
|
.rst_n_i (rst_pclk_n_i),
|
||||||
.clk_aud_i (clk_aud_i),
|
.clk_aud_i (clk_aud_i),
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ module soc_data_plane (
|
|||||||
s_resource_block_hp[6],
|
s_resource_block_hp[6],
|
||||||
1'b0,
|
1'b0,
|
||||||
s_resource_block_hp[4] || s_resource_block_hp[3],
|
s_resource_block_hp[4] || s_resource_block_hp[3],
|
||||||
s_resource_block_hp[7] || s_resource_block_hp[2] || s_resource_block_hp[1],
|
s_resource_block_hp[2] || s_resource_block_hp[1],
|
||||||
s_resource_block_hp[0],
|
s_resource_block_hp[0],
|
||||||
2'b00
|
2'b00
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,534 @@
|
|||||||
|
"""APU V1 ABI digest tool: canonical table inventory, serialization, and CRC-32.
|
||||||
|
|
||||||
|
Per docs/ip/apu.md (ABI_DIGEST at offset 0x054 and the Phase8 completion
|
||||||
|
contract) the digest is the nonzero CRC-32 over the complete frozen V1 tables:
|
||||||
|
|
||||||
|
- the APB V1 register/field table and the 128-byte job-descriptor layout
|
||||||
|
(canonical source rtl/ip/multimedia/apu_define.svh, mirrored by
|
||||||
|
crt/include/retrosoc/hal/apu_regs.h; both sides are re-verified equal here),
|
||||||
|
- the hardware discovery values (IP_ID, IP_VERSION, CAPABILITY0/1, IRQ mask)
|
||||||
|
reported by the current release (P7-qualified configuration),
|
||||||
|
- the format/output-mode/PCM-format/operation identifier tables
|
||||||
|
(crt/include/retrosoc/hal/apu.h enums; operation IDs 0/1 are the frozen V1
|
||||||
|
contract values of docs/ip/apu.md "Descriptor ABI" and have no named code
|
||||||
|
constant),
|
||||||
|
- the supported APUMC V1/V2 container constants (scripts/apu_isa.py),
|
||||||
|
- the APUM model container constants (scripts/apu_kws.py),
|
||||||
|
- the microcode class/opcode/predicate/wait-source/primitive/trap tables, the
|
||||||
|
instruction field positions, the entry word layout, and the P3/P4/P5
|
||||||
|
primitive masks (scripts/apu_isa.py abi_manifest()),
|
||||||
|
- the reserved MP3 identifiers: format ID 1, capability bit 1, the three-entry
|
||||||
|
APUMC layout whose entry 1 is the unsupported-MP3 trap stub
|
||||||
|
(rtl/ip/multimedia/apu_p5_codecs.apus), the trap instruction/detail, and the
|
||||||
|
P5 rejection tuple (code 3/stage 2/detail 0x01000001).
|
||||||
|
|
||||||
|
The deferred P6 design archive is documentation-only and contributes nothing;
|
||||||
|
no P6 constant exists in any parsed source.
|
||||||
|
|
||||||
|
Serialization (the release identity; keep stable): canonical ASCII JSON with
|
||||||
|
recursively sorted keys, separators `,` and `:`, no insignificant whitespace,
|
||||||
|
no terminal newline, and every integer encoded as a JSON decimal number. The
|
||||||
|
digest is the reflected CRC-32/ISO-HDLC (binascii.crc32, the same variant as
|
||||||
|
apu_isa.crc32_iso_hdlc) over that complete byte stream.
|
||||||
|
|
||||||
|
CLI: default prints the digest and section summary; --check compares the
|
||||||
|
computed digest against the handwritten rtl/ip/multimedia/apu_reg.sv AbiDigest
|
||||||
|
localparam and the crt/include/retrosoc/hal/apu_regs.h RS_APU_DIGEST_IMPLEMENTED
|
||||||
|
define, requiring all three to match and be nonzero; --json PATH dumps the
|
||||||
|
canonical inventory for review.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
import apu_kws # noqa: E402
|
||||||
|
from apu_isa import ( # noqa: E402
|
||||||
|
APUMC_ABI_V2,
|
||||||
|
ControlOpcode,
|
||||||
|
Entry,
|
||||||
|
Instruction,
|
||||||
|
InstructionClass,
|
||||||
|
abi_manifest,
|
||||||
|
)
|
||||||
|
|
||||||
|
RTL_DEFINE = ROOT / "rtl/ip/multimedia/apu_define.svh"
|
||||||
|
RTL_REGISTER = ROOT / "rtl/ip/multimedia/apu_reg.sv"
|
||||||
|
RTL_MICROCODE = ROOT / "rtl/ip/multimedia/apu_p5_codecs.apus"
|
||||||
|
C_HEADER = ROOT / "crt/include/retrosoc/hal/apu_regs.h"
|
||||||
|
C_HAL = ROOT / "crt/include/retrosoc/hal/apu.h"
|
||||||
|
|
||||||
|
SECTION_ORDER = (
|
||||||
|
"discovery",
|
||||||
|
"apb",
|
||||||
|
"kws_model",
|
||||||
|
"formats",
|
||||||
|
"apumc",
|
||||||
|
"apum",
|
||||||
|
"isa",
|
||||||
|
"mp3_stub",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rtl_defines() -> dict[str, int]:
|
||||||
|
"""Parse every `define in the canonical hardware/software ABI header."""
|
||||||
|
|
||||||
|
values: dict[str, int] = {}
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^`define\s+((?:APB4_APU__|RETROSOC_APU_KWS__)\w+)\s+"
|
||||||
|
r"((?:\d+)'[hHdDbB][0-9a-fA-F_]+|\d+)\s*$"
|
||||||
|
)
|
||||||
|
for line in RTL_DEFINE.read_text(encoding="utf-8").splitlines():
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
name, literal = match.groups()
|
||||||
|
if "'" in literal:
|
||||||
|
_, value = literal.split("'", maxsplit=1)
|
||||||
|
radix = {"h": 16, "d": 10, "b": 2}[value[0].lower()]
|
||||||
|
values[name] = int(value[1:].replace("_", ""), radix)
|
||||||
|
else:
|
||||||
|
values[name] = int(literal, 10)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _c_abi_defines() -> dict[str, int]:
|
||||||
|
"""Parse the handwritten RS_APU_ABI_* mirror of the SVH define file."""
|
||||||
|
|
||||||
|
values: dict[str, int] = {}
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^#define\s+RS_APU_ABI_(\w+)\s+"
|
||||||
|
r"(?:UINT32_C\((0[xX][0-9a-fA-F]+)\)|(\d+)[uU]?)\s*$"
|
||||||
|
)
|
||||||
|
for line in C_HEADER.read_text(encoding="utf-8").splitlines():
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
name, hexadecimal, decimal = match.groups()
|
||||||
|
values[name] = int(hexadecimal, 16) if hexadecimal is not None else int(decimal, 10)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _c_discovery_defines() -> dict[str, int]:
|
||||||
|
"""Parse the implemented hardware discovery values from the C header."""
|
||||||
|
|
||||||
|
values: dict[str, int] = {}
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^#define\s+RS_APU_(IP_ID_VALUE|IP_VERSION_VALUE|IRQ_ALL"
|
||||||
|
r"|CAPABILITY0_IMPLEMENTED|CAPABILITY0_P7_IMPLEMENTED"
|
||||||
|
r"|CAPABILITY1_IMPLEMENTED|CAPABILITY1_P7_IMPLEMENTED)"
|
||||||
|
r"\s+UINT32_C\((0[xX][0-9a-fA-F]+)\)\s*$"
|
||||||
|
)
|
||||||
|
for line in C_HEADER.read_text(encoding="utf-8").splitlines():
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match is not None:
|
||||||
|
values[match.group(1)] = int(match.group(2), 16)
|
||||||
|
values["CAPABILITY0_IMPLEMENTED"] = values["CAPABILITY0_P7_IMPLEMENTED"]
|
||||||
|
values["CAPABILITY1_IMPLEMENTED"] = values["CAPABILITY1_P7_IMPLEMENTED"]
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _hex_literal(pattern: str, text: str) -> int:
|
||||||
|
match = re.search(pattern, text)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"pattern not found in {RTL_REGISTER.name}: {pattern}")
|
||||||
|
return int(match.group(1).replace("_", ""), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def _rtl_discovery_values() -> dict[str, int]:
|
||||||
|
"""Extract the release-selected discovery localparams from apu_reg.sv."""
|
||||||
|
|
||||||
|
text = RTL_REGISTER.read_text(encoding="utf-8")
|
||||||
|
branch = r"localparam logic \[31:0\] %s = (?:\([^)]*\)|\w+) \? 32'h([0-9a-fA-F_]+) :"
|
||||||
|
return {
|
||||||
|
"IP_ID_VALUE": _hex_literal(
|
||||||
|
r"localparam logic \[31:0\] IpId = 32'h([0-9a-fA-F_]+);", text
|
||||||
|
),
|
||||||
|
"IP_VERSION_VALUE": _hex_literal(branch % "IpVersion", text),
|
||||||
|
"CAPABILITY0_IMPLEMENTED": _hex_literal(branch % "Capability0", text),
|
||||||
|
"CAPABILITY1_IMPLEMENTED": _hex_literal(branch % "Capability1", text),
|
||||||
|
"IRQ_ALL": _hex_literal(
|
||||||
|
r"localparam logic \[10:0\] IrqMask = 11'h([0-9a-fA-F_]+);", text
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _c_enum_values(enum_name: str) -> dict[str, int]:
|
||||||
|
"""Parse a simple `typedef enum { NAME = n, ... } name;` block from apu.h."""
|
||||||
|
|
||||||
|
text = C_HAL.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"typedef enum \{([^}]*)\}\s+" + enum_name + r"\s*;", text)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"enum {enum_name} not found in {C_HAL.name}")
|
||||||
|
return {
|
||||||
|
name: int(value)
|
||||||
|
for name, value in re.findall(r"(\w+)\s*=\s*(\d+)\s*,?", match.group(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_rtl_exact() -> dict[str, int]:
|
||||||
|
"""Map every apu_isa manifest value to its handwritten SVH define name."""
|
||||||
|
|
||||||
|
manifest = abi_manifest()
|
||||||
|
exact = {
|
||||||
|
"APUMC_MAGIC": manifest["apumc"]["magic"],
|
||||||
|
"APUMC_ABI": manifest["apumc"]["abi"],
|
||||||
|
"APUMC_ABI_V1": manifest["apumc"]["abi_v1"],
|
||||||
|
"APUMC_ABI_V2": manifest["apumc"]["abi_v2"],
|
||||||
|
"APUMC_V1": manifest["apumc"]["abi_v1"],
|
||||||
|
"APUMC_V2": manifest["apumc"]["abi_v2"],
|
||||||
|
"APUMC_HEADER_BYTES": manifest["apumc"]["header_bytes"],
|
||||||
|
"APUMC_ENTRY_BYTES": manifest["apumc"]["entry_bytes"],
|
||||||
|
"APUMC_ENTRY_COUNT": manifest["apumc"]["entry_count"],
|
||||||
|
"APUMC_MAX_INSTRUCTIONS": manifest["apumc"]["max_instructions"],
|
||||||
|
"APUMC_MAX_INSTRUCTIONS_V1": manifest["apumc"]["max_instructions_v1"],
|
||||||
|
"APUMC_MAX_INSTRUCTIONS_V2": manifest["apumc"]["max_instructions_v2"],
|
||||||
|
"APUMC_P3_PRIMITIVE_MASK": manifest["p3_implemented_primitive_mask"],
|
||||||
|
"APUMC_P4_PRIMITIVE_MASK": manifest["p4_implemented_primitive_mask"],
|
||||||
|
"APUMC_P5_PRIMITIVE_MASK": manifest["p5_implemented_primitive_mask"],
|
||||||
|
"LOCAL_DATA_BYTES": manifest["p4_local_data_bytes"],
|
||||||
|
"LOCAL_TABLE_SCRATCH_BYTES": manifest["p4_local_table_scratch_bytes"],
|
||||||
|
}
|
||||||
|
for name, value in manifest["classes"].items():
|
||||||
|
exact[f"MC_CLASS_{name.upper()}"] = value
|
||||||
|
for name, value in manifest["control_opcodes"].items():
|
||||||
|
exact[f"MC_CONTROL_{name.upper()}"] = value
|
||||||
|
for name, value in manifest["scalar_opcodes"].items():
|
||||||
|
exact[f"MC_SCALAR_{name.upper()}"] = value
|
||||||
|
for class_name, opcodes in manifest["deferred_opcodes"].items():
|
||||||
|
for name, value in opcodes.items():
|
||||||
|
exact[f"MC_{class_name.upper()}_{name.upper()}"] = value
|
||||||
|
predicate_names = {
|
||||||
|
"always": "ALWAYS",
|
||||||
|
"eq": "EQ",
|
||||||
|
"ne": "NE",
|
||||||
|
"slt": "SIGNED_LT",
|
||||||
|
"sge": "SIGNED_GE",
|
||||||
|
"ult": "UNSIGNED_LT",
|
||||||
|
"uge": "UNSIGNED_GE",
|
||||||
|
"input_exhausted": "INPUT_EXHAUSTED",
|
||||||
|
"input_ready": "INPUT_READY",
|
||||||
|
"output_ready": "OUTPUT_READY",
|
||||||
|
"kernel_done": "KERNEL_DONE",
|
||||||
|
"transport_done": "TRANSPORT_DONE",
|
||||||
|
}
|
||||||
|
for name, value in manifest["predicates"].items():
|
||||||
|
exact[f"MC_PRED_{predicate_names[name]}"] = value
|
||||||
|
for name, value in manifest["wait_sources"].items():
|
||||||
|
exact[f"MC_WAIT_{name.upper()}"] = value
|
||||||
|
for name, value in manifest["primitives"].items():
|
||||||
|
exact[f"MC_PRIMITIVE_{name.upper()}"] = value
|
||||||
|
for name, value in manifest["instruction_fields"].items():
|
||||||
|
exact[f"MC_INSTRUCTION_{name.upper()}"] = value
|
||||||
|
for name, value in manifest["entry_words"].items():
|
||||||
|
exact[f"APUMC_ENTRY_{name.upper()}"] = value
|
||||||
|
trap_names = {
|
||||||
|
"illegal": "ILLEGAL",
|
||||||
|
"pc_range": "PC_RANGE",
|
||||||
|
"call_stack": "CALL_STACK",
|
||||||
|
"loop": "LOOP",
|
||||||
|
"local_range": "LOCAL_RANGE",
|
||||||
|
"unavailable": "UNAVAILABLE",
|
||||||
|
"watchdog": "WATCHDOG",
|
||||||
|
"retired_budget": "RETIRED_BUDGET",
|
||||||
|
"engine": "ENGINE",
|
||||||
|
"explicit": "EXPLICIT",
|
||||||
|
}
|
||||||
|
for name, value in manifest["trap_reasons"].items():
|
||||||
|
if name != "reserved":
|
||||||
|
exact[f"MC_TRAP_{trap_names[name]}"] = value
|
||||||
|
return exact
|
||||||
|
|
||||||
|
|
||||||
|
def _require_equal(what: str, left: dict[str, int], right: dict[str, int]) -> None:
|
||||||
|
if left != right:
|
||||||
|
only_left = sorted(set(left) - set(right))
|
||||||
|
only_right = sorted(set(right) - set(left))
|
||||||
|
differ = sorted(key for key in set(left) & set(right) if left[key] != right[key])
|
||||||
|
raise ValueError(f"{what} drift: only-left {only_left}, only-right {only_right}, "
|
||||||
|
f"differing {[(key, left[key], right[key]) for key in differ]}")
|
||||||
|
|
||||||
|
|
||||||
|
def _mp3_stub_section(apb: dict[str, int]) -> dict[str, int]:
|
||||||
|
"""Reconstruct the reserved-MP3 APUMC stub contract from the released image."""
|
||||||
|
|
||||||
|
lines = RTL_MICROCODE.read_text(encoding="utf-8").splitlines()
|
||||||
|
entry_lines = [line for line in lines if line.startswith(".entry ")]
|
||||||
|
if len(entry_lines) != 3:
|
||||||
|
raise ValueError(f"{RTL_MICROCODE.name} must declare exactly three .entry lines")
|
||||||
|
mp3_tokens = entry_lines[1].split()
|
||||||
|
if mp3_tokens[:5] != [".entry", "1", "mp3", "mp3", "mp3"]:
|
||||||
|
raise ValueError(f"unexpected MP3 entry directive: {entry_lines[1]}")
|
||||||
|
numbers = [int(token, 0) for token in mp3_tokens[5:]]
|
||||||
|
if numbers != [1, 1, 0, 0, 0, 0, 0]:
|
||||||
|
raise ValueError(f"unexpected MP3 stub entry fields: {entry_lines[1]}")
|
||||||
|
try:
|
||||||
|
label_index = lines.index("mp3:")
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValueError("mp3 label missing from released microcode image") from error
|
||||||
|
body = [line.strip() for line in lines[label_index + 1:] if line.strip()]
|
||||||
|
trap_match = re.fullmatch(r"trap 0x([0-9a-fA-F]+)", body[0])
|
||||||
|
if trap_match is None:
|
||||||
|
raise ValueError(f"mp3 label must be followed by its trap stub, found: {body[0]}")
|
||||||
|
trap_detail = int(trap_match.group(1), 16)
|
||||||
|
if trap_detail != 0x01000001:
|
||||||
|
raise ValueError(f"unexpected MP3 stub trap detail 0x{trap_detail:08x}")
|
||||||
|
|
||||||
|
stub = Entry(
|
||||||
|
format_id=1,
|
||||||
|
entry_pc=0,
|
||||||
|
first_pc=0,
|
||||||
|
last_pc=0,
|
||||||
|
max_loop_count=numbers[0],
|
||||||
|
max_retired=numbers[1],
|
||||||
|
scratch_base=numbers[2],
|
||||||
|
scratch_bytes=numbers[3],
|
||||||
|
primitive_mask=numbers[4],
|
||||||
|
table_offset=numbers[5],
|
||||||
|
table_bytes=numbers[6],
|
||||||
|
)
|
||||||
|
words = stub.words(APUMC_ABI_V2)
|
||||||
|
if words != (0x00000001, 0, 0, 0, 1, 1, 0, 0):
|
||||||
|
raise ValueError(f"MP3 stub entry words drifted: {[hex(word) for word in words]}")
|
||||||
|
trap_instruction = Instruction(
|
||||||
|
instruction_class=int(InstructionClass.CONTROL),
|
||||||
|
opcode=int(ControlOpcode.TRAP),
|
||||||
|
immediate=trap_detail,
|
||||||
|
).encode()
|
||||||
|
if trap_instruction != 0x0200000001000001:
|
||||||
|
raise ValueError(f"MP3 stub trap instruction drifted: 0x{trap_instruction:016x}")
|
||||||
|
|
||||||
|
section = {
|
||||||
|
"format_id": 1,
|
||||||
|
"capability_bit": apb["CAPABILITY0_MP3"],
|
||||||
|
"entry_count": apb["APUMC_ENTRY_COUNT"],
|
||||||
|
"entry_index": 1,
|
||||||
|
"trap_instruction": trap_instruction,
|
||||||
|
"trap_detail": trap_detail,
|
||||||
|
"rejection_error_code": apb["ERROR_CODE_UNSUPPORTED"],
|
||||||
|
"rejection_error_stage": apb["ERROR_STAGE_RING"],
|
||||||
|
"rejection_detail": trap_detail,
|
||||||
|
}
|
||||||
|
for index, word in enumerate(words):
|
||||||
|
section[f"entry_words[{index}]"] = word
|
||||||
|
return section
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_inventory() -> dict[str, dict[str, int]]:
|
||||||
|
"""Build the complete frozen V1 table inventory, failing on any drift."""
|
||||||
|
|
||||||
|
rtl = _rtl_defines()
|
||||||
|
apb = {
|
||||||
|
name.removeprefix("APB4_APU__"): value
|
||||||
|
for name, value in rtl.items()
|
||||||
|
if name.startswith("APB4_APU__")
|
||||||
|
}
|
||||||
|
kws_model = {
|
||||||
|
name.removeprefix("RETROSOC_APU_KWS__"): value
|
||||||
|
for name, value in rtl.items()
|
||||||
|
if name.startswith("RETROSOC_APU_KWS__")
|
||||||
|
}
|
||||||
|
_require_equal("APB define/C mirror", apb, _c_abi_defines())
|
||||||
|
_require_equal("ISA manifest/SVH", _manifest_rtl_exact(),
|
||||||
|
{name: apb[name] for name in _manifest_rtl_exact()})
|
||||||
|
_require_equal(
|
||||||
|
"APUM container/SVH",
|
||||||
|
{
|
||||||
|
"APUM_MAGIC": apu_kws.APUM_MAGIC,
|
||||||
|
"APUM_ABI": apu_kws.APUM_ABI,
|
||||||
|
"APUM_IMAGE_BYTES": apu_kws.APUM_BYTES,
|
||||||
|
"APUM_PAYLOAD_CRC": apu_kws.APUM_PAYLOAD_CRC,
|
||||||
|
},
|
||||||
|
{name: apb[name] for name in ("APUM_MAGIC", "APUM_ABI", "APUM_IMAGE_BYTES",
|
||||||
|
"APUM_PAYLOAD_CRC")},
|
||||||
|
)
|
||||||
|
discovery_c = _c_discovery_defines()
|
||||||
|
discovery_rtl = _rtl_discovery_values()
|
||||||
|
_require_equal("discovery C/RTL", discovery_rtl,
|
||||||
|
{name: discovery_c[name] for name in discovery_rtl})
|
||||||
|
|
||||||
|
formats = {name.lower().removeprefix("rs_apu_"): value
|
||||||
|
for name, value in _c_enum_values("rs_apu_format_t").items()}
|
||||||
|
output_modes = {name.lower().removeprefix("rs_apu_"): value
|
||||||
|
for name, value in _c_enum_values("rs_apu_output_t").items()}
|
||||||
|
pcm_formats = {name.lower().removeprefix("rs_apu_"): value
|
||||||
|
for name, value in _c_enum_values("rs_apu_pcm_t").items()}
|
||||||
|
_require_equal(
|
||||||
|
"format ID/capability bit",
|
||||||
|
{name: formats[name] for name in ("wav", "mp3", "flac")},
|
||||||
|
{name: apb[f"CAPABILITY0_{name.upper()}"] for name in ("wav", "mp3", "flac")},
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest = abi_manifest()
|
||||||
|
isa: dict[str, int] = {}
|
||||||
|
for group in ("classes", "control_opcodes", "scalar_opcodes", "predicates",
|
||||||
|
"wait_sources", "primitives", "trap_reasons", "instruction_fields",
|
||||||
|
"entry_words"):
|
||||||
|
for name, value in manifest[group].items():
|
||||||
|
isa[f"{group}.{name}"] = value
|
||||||
|
for class_name, opcodes in manifest["deferred_opcodes"].items():
|
||||||
|
for name, value in opcodes.items():
|
||||||
|
isa[f"deferred_opcodes.{class_name}.{name}"] = value
|
||||||
|
for phase in ("p3", "p4", "p5"):
|
||||||
|
isa[f"implemented_primitive_mask.{phase}"] = manifest[
|
||||||
|
f"{phase}_implemented_primitive_mask"]
|
||||||
|
isa["local_memory.p4_data_bytes"] = manifest["p4_local_data_bytes"]
|
||||||
|
isa["local_memory.p4_table_scratch_bytes"] = manifest["p4_local_table_scratch_bytes"]
|
||||||
|
|
||||||
|
apumc = {name: value for name, value in manifest["apumc"].items()}
|
||||||
|
apum = {
|
||||||
|
"magic": apu_kws.APUM_MAGIC,
|
||||||
|
"abi": apu_kws.APUM_ABI,
|
||||||
|
"image_bytes": apu_kws.APUM_BYTES,
|
||||||
|
"header_bytes": apu_kws.APUM_HEADER_BYTES,
|
||||||
|
"operator_offset": apu_kws.APUM_OPERATOR_OFFSET,
|
||||||
|
"operator_count": apu_kws.APUM_OPERATOR_COUNT,
|
||||||
|
"operator_bytes": apu_kws.APUM_OPERATOR_BYTES,
|
||||||
|
"tensor_offset": apu_kws.APUM_TENSOR_OFFSET,
|
||||||
|
"tensor_count": apu_kws.APUM_TENSOR_COUNT,
|
||||||
|
"tensor_bytes": apu_kws.APUM_TENSOR_BYTES,
|
||||||
|
"parameter_offset": apu_kws.APUM_PARAMETER_OFFSET,
|
||||||
|
"parameter_bytes": apu_kws.APUM_PARAMETER_BYTES,
|
||||||
|
"parameter_end": apu_kws.APUM_PARAMETER_END,
|
||||||
|
"payload_crc": apu_kws.APUM_PAYLOAD_CRC,
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery = {
|
||||||
|
"ip_id": discovery_c["IP_ID_VALUE"],
|
||||||
|
"ip_version": discovery_c["IP_VERSION_VALUE"],
|
||||||
|
"capability0": discovery_c["CAPABILITY0_IMPLEMENTED"],
|
||||||
|
"capability0_p7": discovery_c["CAPABILITY0_P7_IMPLEMENTED"],
|
||||||
|
"capability1": discovery_c["CAPABILITY1_IMPLEMENTED"],
|
||||||
|
"capability1_p7": discovery_c["CAPABILITY1_P7_IMPLEMENTED"],
|
||||||
|
"irq_all": discovery_c["IRQ_ALL"],
|
||||||
|
}
|
||||||
|
format_section = {f"format.{name}": value for name, value in formats.items()}
|
||||||
|
format_section.update({f"output_mode.{name}": value
|
||||||
|
for name, value in output_modes.items()})
|
||||||
|
format_section.update({f"pcm_format.{name}": value
|
||||||
|
for name, value in pcm_formats.items()})
|
||||||
|
format_section["operation.file_decode"] = 0
|
||||||
|
format_section["operation.kws_memory_window"] = 1
|
||||||
|
|
||||||
|
inventory = {
|
||||||
|
"discovery": discovery,
|
||||||
|
"apb": apb,
|
||||||
|
"kws_model": kws_model,
|
||||||
|
"formats": format_section,
|
||||||
|
"apumc": apumc,
|
||||||
|
"apum": apum,
|
||||||
|
"isa": isa,
|
||||||
|
"mp3_stub": _mp3_stub_section(apb),
|
||||||
|
}
|
||||||
|
if tuple(inventory) != SECTION_ORDER:
|
||||||
|
raise ValueError("section order drift")
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_inventory(inventory: dict[str, dict[str, int]]) -> bytes:
|
||||||
|
"""Serialize the inventory to the documented release-identity byte stream."""
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
inventory,
|
||||||
|
ensure_ascii=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
sort_keys=True,
|
||||||
|
).encode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_abi_digest() -> int:
|
||||||
|
"""Return the CRC-32/ISO-HDLC digest over the serialized V1 tables."""
|
||||||
|
|
||||||
|
return binascii.crc32(serialize_inventory(canonical_inventory())) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def rtl_implemented_digest() -> int:
|
||||||
|
"""Extract the handwritten AbiDigest localparam from apu_reg.sv."""
|
||||||
|
|
||||||
|
text = RTL_REGISTER.read_text(encoding="utf-8")
|
||||||
|
match = re.search(
|
||||||
|
r"localparam logic \[31:0\] AbiDigest = EnableP7 \? "
|
||||||
|
r"32'h([0-9a-fA-F_]+) : 32'd0;",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"AbiDigest localparam not found in {RTL_REGISTER.name}")
|
||||||
|
return int(match.group(1).replace("_", ""), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def c_implemented_digest() -> int:
|
||||||
|
"""Extract the handwritten P7 release digest from apu_regs.h."""
|
||||||
|
|
||||||
|
text = C_HEADER.read_text(encoding="utf-8")
|
||||||
|
match = re.search(
|
||||||
|
r"^#define\s+RS_APU_DIGEST_P7_IMPLEMENTED\s+UINT32_C\((0[xX][0-9a-fA-F]+)\)\s*$",
|
||||||
|
text,
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"RS_APU_DIGEST_P7_IMPLEMENTED not found in {C_HEADER.name}")
|
||||||
|
return int(match.group(1), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
parser.add_argument("--check", action="store_true",
|
||||||
|
help="require RTL/C handwritten constants to match the computed digest")
|
||||||
|
parser.add_argument("--json", metavar="PATH",
|
||||||
|
help="dump the canonical inventory as JSON for review")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
try:
|
||||||
|
inventory = canonical_inventory()
|
||||||
|
digest = binascii.crc32(serialize_inventory(inventory)) & 0xFFFFFFFF
|
||||||
|
except ValueError as error:
|
||||||
|
print(f"apu_abi_digest: error: {error}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.json is not None:
|
||||||
|
payload = {
|
||||||
|
"digest": f"0x{digest:08x}",
|
||||||
|
"serialization": "canonical ASCII JSON; recursively sorted keys; compact "
|
||||||
|
"separators; decimal integers; no terminal newline; "
|
||||||
|
"CRC-32/ISO-HDLC",
|
||||||
|
"sections": {
|
||||||
|
name: {key: f"0x{value:016x}" for key, value in sorted(entries.items())}
|
||||||
|
for name, entries in inventory.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Path(args.json).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||||
|
print(f"wrote canonical inventory to {args.json}")
|
||||||
|
|
||||||
|
print(f"0x{digest:08x}")
|
||||||
|
for name in SECTION_ORDER:
|
||||||
|
print(f" {name}: {len(inventory[name])} entries")
|
||||||
|
print(f" serialized: {len(serialize_inventory(inventory))} bytes")
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
try:
|
||||||
|
rtl_digest = rtl_implemented_digest()
|
||||||
|
c_digest = c_implemented_digest()
|
||||||
|
except ValueError as error:
|
||||||
|
print(f"apu_abi_digest: error: {error}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
ok = digest != 0 and rtl_digest == digest and c_digest == digest
|
||||||
|
print(f" computed: 0x{digest:08x}")
|
||||||
|
print(f" apu_reg.sv: 0x{rtl_digest:08x}")
|
||||||
|
print(f" apu_regs.h: 0x{c_digest:08x}")
|
||||||
|
if not ok:
|
||||||
|
print("apu_abi_digest: FAIL: digest mismatch or zero digest", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print("apu_abi_digest: OK: computed digest matches both handwritten constants")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"""Bit-accurate model (BAM) of the frozen APU-P7 frontend numerical profile 1.
|
||||||
|
|
||||||
|
Implements docs/ip/apu.md "P7 frontend numerical profile 1" (lines 2322-2398)
|
||||||
|
exactly, mirroring the RTL arithmetic in rtl/ip/multimedia/apu_kws_engine.sv
|
||||||
|
operation by operation:
|
||||||
|
|
||||||
|
PCM 16000 x S16 -> positive peak P=max(1,max(x[0..15999]))
|
||||||
|
-> 49 frames of 480 samples (hop 320; the last 160 samples only feed P)
|
||||||
|
-> Hann Q2.30, real FFT input RNE(x*H/2^15), imaginary zero, zero-pad 512
|
||||||
|
-> bit-reversed radix-2 DIT FFT (Q2.30 cos/-sin twiddles, one RNE/2^30 per
|
||||||
|
complex multiply, RNE/2 per butterfly sum/diff; components = DFT*64,
|
||||||
|
signed-32 with fault on overflow)
|
||||||
|
-> bins 0..256, nearest-integer sqrt of the unsigned-64 component squares
|
||||||
|
(ties even)
|
||||||
|
-> 40 mel triangles Q2.30 (DC bin zeroed), M=RNE(sum/2^32) unsigned UQ28.4
|
||||||
|
-> exact rational z=(1000000*M+16*P)/(16000000*P), exact 2^e*m
|
||||||
|
normalization, log ROM interpolation with f=65536 allowed
|
||||||
|
-> DCT Q2.30, C[j]=RNE(sum(log*D)/2^30) signed Q8.24
|
||||||
|
-> q=TZ(C[j]/(2^24*S)+83) with the exact binary32 scale S=0x3f15af17
|
||||||
|
(2^24*S == 9809687 == 0x95AF17), low-8-bit wrap to the INT8 feature byte.
|
||||||
|
|
||||||
|
All arithmetic is exact Python integer arithmetic. ROM constants are imported
|
||||||
|
from scripts/generate_apu_kws_rtl_constants.py (the same generators that emit
|
||||||
|
rtl/ip/multimedia/apu_kws_rom.svh), never duplicated. Where the RTL faults
|
||||||
|
(FFT signed-32 component overflow) or wraps (log output truncation to
|
||||||
|
signed-32), this model does the same; :class:`FrontendFault` marks the fault
|
||||||
|
cases, which valid S16 audio never reaches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
|
import generate_apu_kws_rtl_constants as gen # noqa: E402
|
||||||
|
|
||||||
|
WINDOW_SAMPLES: Final = 16000
|
||||||
|
WINDOW_BYTES: Final = 32000
|
||||||
|
FRAME_COUNT: Final = 49
|
||||||
|
FRAME_SAMPLES: Final = 480
|
||||||
|
FRAME_HOP: Final = 320
|
||||||
|
FFT_POINTS: Final = 512
|
||||||
|
FFT_BINS: Final = 257
|
||||||
|
MEL_BANDS: Final = 40
|
||||||
|
MFCC_COEFFICIENTS: Final = 10
|
||||||
|
FEATURE_BYTES: Final = FRAME_COUNT * MFCC_COEFFICIENTS
|
||||||
|
QUANT_SCALE_BITS: Final = 0x3F15AF17
|
||||||
|
QUANT_DIVISOR: Final = 0x0095AF17
|
||||||
|
QUANT_BIAS: Final = 83
|
||||||
|
_S32_MIN: Final = -(1 << 31)
|
||||||
|
_S32_MAX: Final = (1 << 31) - 1
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendFault(RuntimeError):
|
||||||
|
"""The input drives an RTL checked-arithmetic fault range."""
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def hann_q30() -> tuple[int, ...]:
|
||||||
|
"""Hann window coefficients H[n]=RNE(2^30*(1-cos(2*pi*n/480))/2)."""
|
||||||
|
return tuple(gen._hann())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def twiddle_q30() -> tuple[tuple[int, ...], tuple[int, ...]]:
|
||||||
|
"""FFT twiddles: RNE-quantized Q2.30 cos and negative sin of 2*pi*k/512."""
|
||||||
|
real, imag = gen._twiddle()
|
||||||
|
return tuple(real), tuple(imag)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def mel_q30() -> tuple[int, ...]:
|
||||||
|
"""Forty mel triangle weight rows, 257 bins each, Q2.30, DC zeroed (flat)."""
|
||||||
|
return tuple(gen._mel_weights())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def dct_q30() -> tuple[int, ...]:
|
||||||
|
"""DCT coefficients D[j,b]=RNE(2^30*sqrt(2/40)*cos(pi*(b+1/2)*j/40)) (flat)."""
|
||||||
|
return tuple(gen._dct())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def log_q24() -> tuple[int, ...]:
|
||||||
|
"""Log ROM L[i]=RNE(2^24*ln(1+i/1024)) for i=0..1024, signed Q8.24."""
|
||||||
|
return tuple(gen._log_rom())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def ln2_q24() -> int:
|
||||||
|
"""LN2=RNE(2^24*ln(2)) signed Q8.24, the ROM localparam ApuKwsLn2Q24."""
|
||||||
|
return gen._rne(gen.TWO.ln() * Decimal(1 << 24))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _bit_reverse9() -> tuple[int, ...]:
|
||||||
|
return tuple(int(f"{index:09b}"[::-1], 2) for index in range(FFT_POINTS))
|
||||||
|
|
||||||
|
|
||||||
|
def _s32(value: int) -> int:
|
||||||
|
"""Truncate to signed 32 bits, mirroring the RTL 32'(...) conversion."""
|
||||||
|
value &= 0xFFFFFFFF
|
||||||
|
return value - (1 << 32) if value > _S32_MAX else value
|
||||||
|
|
||||||
|
|
||||||
|
def _trunc_div(numerator: int, denominator: int) -> int:
|
||||||
|
"""TZ(numerator/denominator): truncation toward zero."""
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def rne_shift(value: int, shift: int) -> int:
|
||||||
|
"""RNE(value / 2^shift) with ties to even, sign symmetric (RTL rne_shift)."""
|
||||||
|
if shift == 0:
|
||||||
|
return value
|
||||||
|
magnitude = -value if value < 0 else value
|
||||||
|
quotient = magnitude >> shift
|
||||||
|
remainder = magnitude & ((1 << shift) - 1)
|
||||||
|
half = 1 << (shift - 1)
|
||||||
|
if remainder > half or (remainder == half and (quotient & 1) != 0):
|
||||||
|
quotient += 1
|
||||||
|
return -quotient if value < 0 else quotient
|
||||||
|
|
||||||
|
|
||||||
|
def rne_uq32(value: int) -> int:
|
||||||
|
"""Unsigned RNE(value / 2^32) with ties to even (RTL rne_uq32)."""
|
||||||
|
if value < 0 or value >= 1 << 64:
|
||||||
|
raise FrontendFault(f"mel reduction outside u64: {value}")
|
||||||
|
quotient = value >> 32
|
||||||
|
remainder = value & 0xFFFFFFFF
|
||||||
|
if remainder > 0x80000000 or (remainder == 0x80000000 and (quotient & 1) != 0):
|
||||||
|
quotient += 1
|
||||||
|
return quotient
|
||||||
|
|
||||||
|
|
||||||
|
def nearest_isqrt(value: int) -> int:
|
||||||
|
"""Nearest-integer square root of an unsigned-64 value, ties even."""
|
||||||
|
if value < 0 or value >= 1 << 64:
|
||||||
|
raise FrontendFault(f"magnitude square outside u64: {value}")
|
||||||
|
root = math.isqrt(value)
|
||||||
|
lower = value - root * root
|
||||||
|
upper = (root + 1) * (root + 1) - value
|
||||||
|
if upper < lower or (upper == lower and (root & 1) != 0):
|
||||||
|
root += 1
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def log_mel_q24(mel: int, peak: int) -> int:
|
||||||
|
"""Exact rational log stage: Q8.24 ln(M/(16*P)+1e-6) (RTL log_mel_q24)."""
|
||||||
|
if not 0 <= mel <= 0xFFFFFFFF:
|
||||||
|
raise FrontendFault(f"mel value outside UQ28.4 word: {mel}")
|
||||||
|
if not 1 <= peak <= 0xFFFF:
|
||||||
|
raise FrontendFault(f"window peak outside u16: {peak}")
|
||||||
|
numerator = 1_000_000 * mel + 16 * peak
|
||||||
|
denominator = 16_000_000 * peak
|
||||||
|
exponent = 0
|
||||||
|
if numerator >= denominator:
|
||||||
|
normalized = denominator
|
||||||
|
while numerator >= normalized << 1:
|
||||||
|
normalized <<= 1
|
||||||
|
exponent += 1
|
||||||
|
else:
|
||||||
|
normalized = denominator
|
||||||
|
while numerator < normalized:
|
||||||
|
numerator <<= 1
|
||||||
|
exponent -= 1
|
||||||
|
scaled = (numerator - normalized) * 1024
|
||||||
|
index = scaled // normalized
|
||||||
|
remainder = scaled - index * normalized
|
||||||
|
fraction_scaled = remainder * 65536
|
||||||
|
fraction = fraction_scaled // normalized
|
||||||
|
fraction_remainder = fraction_scaled - fraction * normalized
|
||||||
|
if 2 * fraction_remainder > normalized or (
|
||||||
|
2 * fraction_remainder == normalized and (fraction & 1) != 0
|
||||||
|
):
|
||||||
|
fraction += 1 # fraction may be 65536 (spec step 5)
|
||||||
|
table = log_q24()
|
||||||
|
interpolation = rne_shift((table[index + 1] - table[index]) * fraction, 16)
|
||||||
|
return _s32(exponent * ln2_q24() + table[index] + interpolation)
|
||||||
|
|
||||||
|
|
||||||
|
def quantize_coefficient(coefficient: int) -> int:
|
||||||
|
"""Frozen step-7 quantizer: q=TZ(C/(2^24*S)+83) with S=0x3f15af17.
|
||||||
|
|
||||||
|
C/(2^24*S)+83 == (C + 83*9809687)/9809687 as an exact rational; truncating
|
||||||
|
the integer division matches both the frozen formula and the RTL
|
||||||
|
expression (s_dct_coefficient + 64'sh3087_c475) / 64'sh0095_af17.
|
||||||
|
"""
|
||||||
|
return _trunc_div(coefficient + QUANT_BIAS * QUANT_DIVISOR, QUANT_DIVISOR)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_int8(value: int) -> int:
|
||||||
|
"""Low-8-bit wrap of a quantized coefficient, interpreted signed INT8."""
|
||||||
|
wrapped = value & 0xFF
|
||||||
|
return wrapped - 256 if wrapped >= 128 else wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def samples_from_pcm(pcm: bytes) -> tuple[int, ...]:
|
||||||
|
"""Decode one 32000-byte little-endian S16 window into 16000 samples."""
|
||||||
|
if len(pcm) != WINDOW_BYTES:
|
||||||
|
raise ValueError(f"profile-1 window must be {WINDOW_BYTES} bytes, got {len(pcm)}")
|
||||||
|
return struct.unpack(f"<{WINDOW_SAMPLES}h", pcm)
|
||||||
|
|
||||||
|
|
||||||
|
def _fft_frame(windowed: list[int], real: list[int], imag: list[int]) -> None:
|
||||||
|
"""One 512-point profile-1 FFT in place; windowed holds 480 FFT inputs."""
|
||||||
|
twiddle_real, twiddle_imag = twiddle_q30()
|
||||||
|
bit_reverse = _bit_reverse9()
|
||||||
|
for index in range(FFT_POINTS):
|
||||||
|
real[index] = 0
|
||||||
|
imag[index] = 0
|
||||||
|
for index in range(FRAME_SAMPLES):
|
||||||
|
real[bit_reverse[index]] = windowed[index]
|
||||||
|
length = 2
|
||||||
|
while length <= FFT_POINTS:
|
||||||
|
half = length // 2
|
||||||
|
step = FFT_POINTS // length
|
||||||
|
for group in range(0, FFT_POINTS, length):
|
||||||
|
for j in range(half):
|
||||||
|
left = group + j
|
||||||
|
right = left + half
|
||||||
|
wr = twiddle_real[step * j]
|
||||||
|
wi = twiddle_imag[step * j]
|
||||||
|
right_r = real[right]
|
||||||
|
right_i = imag[right]
|
||||||
|
tw_r = _s32(rne_shift(right_r * wr - right_i * wi, 30))
|
||||||
|
tw_i = _s32(rne_shift(right_r * wi + right_i * wr, 30))
|
||||||
|
left_r = real[left]
|
||||||
|
left_i = imag[left]
|
||||||
|
sum_r = rne_shift(left_r + tw_r, 1)
|
||||||
|
sum_i = rne_shift(left_i + tw_i, 1)
|
||||||
|
dif_r = rne_shift(left_r - tw_r, 1)
|
||||||
|
dif_i = rne_shift(left_i - tw_i, 1)
|
||||||
|
for component in (sum_r, sum_i, dif_r, dif_i):
|
||||||
|
if component < _S32_MIN or component > _S32_MAX:
|
||||||
|
raise FrontendFault(f"FFT component overflow: {component}")
|
||||||
|
real[left] = sum_r
|
||||||
|
imag[left] = sum_i
|
||||||
|
real[right] = dif_r
|
||||||
|
imag[right] = dif_i
|
||||||
|
length *= 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FrontendStages:
|
||||||
|
"""Every observable profile-1 stage for one 16000-sample window."""
|
||||||
|
|
||||||
|
samples: tuple[int, ...]
|
||||||
|
peak: int
|
||||||
|
windowed: tuple[tuple[int, ...], ...] # 49 x 480, RNE(x*H/2^15)
|
||||||
|
fft_real: tuple[tuple[int, ...], ...] # 49 x 512 signed-32, unscaled DFT*64
|
||||||
|
fft_imag: tuple[tuple[int, ...], ...]
|
||||||
|
magnitude: tuple[tuple[int, ...], ...] # 49 x 257
|
||||||
|
mel: tuple[tuple[int, ...], ...] # 49 x 40 unsigned UQ28.4
|
||||||
|
logs: tuple[tuple[int, ...], ...] # 49 x 40 signed Q8.24
|
||||||
|
coeffs: tuple[tuple[int, ...], ...] # 49 x 10 signed Q8.24
|
||||||
|
quantized: tuple[tuple[int, ...], ...] # 49 x 10 pre-wrap q
|
||||||
|
features: bytes # 490 wrapped INT8 feature bytes
|
||||||
|
|
||||||
|
|
||||||
|
def frontend_stages(samples: tuple[int, ...] | list[int]) -> FrontendStages:
|
||||||
|
"""Run profile 1 on one 16000-sample S16 window and return every stage."""
|
||||||
|
if len(samples) != WINDOW_SAMPLES:
|
||||||
|
raise ValueError(f"profile-1 window must be {WINDOW_SAMPLES} samples")
|
||||||
|
samples = tuple(int(value) for value in samples)
|
||||||
|
if any(value < -32768 or value > 32767 for value in samples):
|
||||||
|
raise ValueError("profile-1 input must be signed S16")
|
||||||
|
hann = hann_q30()
|
||||||
|
mel_weights = mel_q30()
|
||||||
|
dct = dct_q30()
|
||||||
|
peak = max(1, max(samples))
|
||||||
|
|
||||||
|
windowed_rows: list[tuple[int, ...]] = []
|
||||||
|
fft_real_rows: list[tuple[int, ...]] = []
|
||||||
|
fft_imag_rows: list[tuple[int, ...]] = []
|
||||||
|
magnitude_rows: list[tuple[int, ...]] = []
|
||||||
|
mel_rows: list[tuple[int, ...]] = []
|
||||||
|
real = [0] * FFT_POINTS
|
||||||
|
imag = [0] * FFT_POINTS
|
||||||
|
for row in range(FRAME_COUNT):
|
||||||
|
frame = samples[FRAME_HOP * row : FRAME_HOP * row + FRAME_SAMPLES]
|
||||||
|
windowed = [rne_shift(value * coefficient, 15) for value, coefficient in zip(frame, hann)]
|
||||||
|
_fft_frame(windowed, real, imag)
|
||||||
|
windowed_rows.append(tuple(windowed))
|
||||||
|
fft_real_rows.append(tuple(real))
|
||||||
|
fft_imag_rows.append(tuple(imag))
|
||||||
|
magnitude = tuple(
|
||||||
|
nearest_isqrt(real[bin_index] * real[bin_index] + imag[bin_index] * imag[bin_index])
|
||||||
|
for bin_index in range(FFT_BINS)
|
||||||
|
)
|
||||||
|
magnitude_rows.append(magnitude)
|
||||||
|
mel_rows.append(
|
||||||
|
tuple(
|
||||||
|
rne_uq32(
|
||||||
|
sum(
|
||||||
|
magnitude[bin_index] * mel_weights[band * FFT_BINS + bin_index]
|
||||||
|
for bin_index in range(FFT_BINS)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for band in range(MEL_BANDS)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logs_rows = tuple(
|
||||||
|
tuple(log_mel_q24(mel_rows[row][band], peak) for band in range(MEL_BANDS))
|
||||||
|
for row in range(FRAME_COUNT)
|
||||||
|
)
|
||||||
|
coeffs_rows: list[tuple[int, ...]] = []
|
||||||
|
for row in range(FRAME_COUNT):
|
||||||
|
coeffs = []
|
||||||
|
for coefficient in range(MFCC_COEFFICIENTS):
|
||||||
|
total = sum(
|
||||||
|
logs_rows[row][band] * dct[coefficient * MEL_BANDS + band]
|
||||||
|
for band in range(MEL_BANDS)
|
||||||
|
)
|
||||||
|
if total < -(1 << 63) or total >= 1 << 63:
|
||||||
|
raise FrontendFault(f"DCT reduction outside s64: {total}")
|
||||||
|
coeffs.append(rne_shift(total, 30))
|
||||||
|
coeffs_rows.append(tuple(coeffs))
|
||||||
|
quantized_rows = tuple(
|
||||||
|
tuple(quantize_coefficient(coeffs_rows[row][j]) for j in range(MFCC_COEFFICIENTS))
|
||||||
|
for row in range(FRAME_COUNT)
|
||||||
|
)
|
||||||
|
features = bytes(value & 0xFF for row in quantized_rows for value in row)
|
||||||
|
return FrontendStages(
|
||||||
|
samples=samples,
|
||||||
|
peak=peak,
|
||||||
|
windowed=tuple(windowed_rows),
|
||||||
|
fft_real=tuple(fft_real_rows),
|
||||||
|
fft_imag=tuple(fft_imag_rows),
|
||||||
|
magnitude=tuple(magnitude_rows),
|
||||||
|
mel=tuple(mel_rows),
|
||||||
|
logs=logs_rows,
|
||||||
|
coeffs=tuple(coeffs_rows),
|
||||||
|
quantized=quantized_rows,
|
||||||
|
features=features,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mfcc_profile1(pcm: bytes) -> bytes:
|
||||||
|
"""Map one 32000-byte padded PCM window to its 490 profile-1 MFCC bytes."""
|
||||||
|
return frontend_stages(samples_from_pcm(pcm)).features
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the embedded APU release-evidence assets for the apu_release app.
|
||||||
|
|
||||||
|
Reads the deterministic P5 APUMC microcode bundle, the frozen P7 APUM KWS
|
||||||
|
model, and one locked KWS corpus utterance; synthesizes a small deterministic
|
||||||
|
mono 48 kHz S16 WAV fixture; computes the frozen expected PCM through the
|
||||||
|
bit-accurate codec model; and emits a C header with every byte array and CRC
|
||||||
|
the LP/HP evidence flow needs. The generator is deterministic: no network, no
|
||||||
|
clock, and no randomness.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
from apu_codecs import decode_wav, process_pcm # noqa: E402
|
||||||
|
from apu_kws import APUM_ABI, APUM_BYTES, APUM_MAGIC, APUM_PAYLOAD_CRC, APUM_SHA256 # noqa: E402
|
||||||
|
|
||||||
|
APUMC_MAGIC = 0x41504D43
|
||||||
|
APUMC_ABI_VERSIONS = (0x00010000, 0x00020000)
|
||||||
|
APUMC_HEADER_BYTES = 64
|
||||||
|
APUMC_HEADER_CRC_WORD = 11
|
||||||
|
|
||||||
|
WAV_RATE = 48000
|
||||||
|
WAV_CHANNELS = 1
|
||||||
|
WAV_BITS = 16
|
||||||
|
WAV_FRAMES = 480
|
||||||
|
|
||||||
|
KWS_WINDOW_BYTES = 32000
|
||||||
|
KWS_EXPECTED_CLASS = 7 # "Stop" in the frozen class order
|
||||||
|
KWS_THRESHOLD = 1
|
||||||
|
KWS_DEBOUNCE = 1
|
||||||
|
# Locked corpus manifest row 000000 (docs/ip/apu-kws-corpus.tsv).
|
||||||
|
KWS_CORPUS_RELPATH = "stop/563aa4e6_nohash_3.wav"
|
||||||
|
KWS_WAV_SHA256 = "6ed51beca5b58fbbb06a16af68f120066218cf52c3fd4237f222a8ea9c972199"
|
||||||
|
KWS_PCM_SHA256 = "d843b234a0f585d143c88aed0acc1cca5ed02150f3cf5b77cb0489ad5429d725"
|
||||||
|
|
||||||
|
|
||||||
|
def _require(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _crc32(data: bytes) -> int:
|
||||||
|
return zlib.crc32(data) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(data: bytes) -> str:
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_apumc(path: Path) -> tuple[bytes, int, int]:
|
||||||
|
image = path.read_bytes()
|
||||||
|
_require(len(image) >= APUMC_HEADER_BYTES, f"APUMC bundle too small: {path}")
|
||||||
|
magic, abi = struct.unpack_from("<II", image, 0)
|
||||||
|
_require(magic == APUMC_MAGIC, f"APUMC magic mismatch in {path}")
|
||||||
|
_require(abi in APUMC_ABI_VERSIONS, f"APUMC ABI 0x{abi:08x} is not supported")
|
||||||
|
header_crc = struct.unpack_from("<I", image, APUMC_HEADER_CRC_WORD * 4)[0]
|
||||||
|
payload_crc = _crc32(image[APUMC_HEADER_BYTES:])
|
||||||
|
_require(header_crc == payload_crc, f"APUMC header CRC disagrees with payload CRC in {path}")
|
||||||
|
return image, _crc32(image), payload_crc
|
||||||
|
|
||||||
|
|
||||||
|
def _load_apum(path: Path) -> tuple[bytes, int]:
|
||||||
|
image = path.read_bytes()
|
||||||
|
_require(len(image) == APUM_BYTES, f"APUM model is not {APUM_BYTES} bytes: {path}")
|
||||||
|
magic, abi = struct.unpack_from("<II", image, 0)
|
||||||
|
_require(magic == APUM_MAGIC, f"APUM magic mismatch in {path}")
|
||||||
|
_require(abi == APUM_ABI, f"APUM ABI 0x{abi:08x} mismatch in {path}")
|
||||||
|
payload_crc = struct.unpack_from("<I", image, 0x24)[0]
|
||||||
|
_require(payload_crc == APUM_PAYLOAD_CRC, f"APUM payload CRC mismatch in {path}")
|
||||||
|
_require(_sha256(image) == APUM_SHA256, f"APUM image SHA-256 mismatch in {path}")
|
||||||
|
return image, _crc32(image)
|
||||||
|
|
||||||
|
|
||||||
|
def _synthesize_wav() -> bytes:
|
||||||
|
samples = [((index * 48271 + 12345) % 65536) - 32768 for index in range(WAV_FRAMES)]
|
||||||
|
payload = struct.pack(f"<{WAV_FRAMES}h", *samples)
|
||||||
|
alignment = WAV_CHANNELS * (WAV_BITS // 8)
|
||||||
|
fmt = struct.pack("<HHIIHH", 1, WAV_CHANNELS, WAV_RATE, WAV_RATE * alignment, alignment, WAV_BITS)
|
||||||
|
chunks = b"fmt " + struct.pack("<I", len(fmt)) + fmt
|
||||||
|
chunks += b"data" + struct.pack("<I", len(payload)) + payload
|
||||||
|
return b"RIFF" + struct.pack("<I", len(chunks) + 4) + b"WAVE" + chunks
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_pcm(wav: bytes) -> tuple[int, int, int]:
|
||||||
|
audio = decode_wav(wav, strict=True)
|
||||||
|
processed = process_pcm(audio, output_bits=16)
|
||||||
|
_require(processed.rate == WAV_RATE, "fixture rate mismatch")
|
||||||
|
_require(processed.channels == WAV_CHANNELS, "fixture channel mismatch")
|
||||||
|
_require(processed.frames == WAV_FRAMES, "fixture frame mismatch")
|
||||||
|
_require(len(processed.payload) == WAV_FRAMES * 2, "fixture payload size mismatch")
|
||||||
|
info = audio.info
|
||||||
|
_require(info.input_used == len(wav), "fixture WAV is not fully consumed")
|
||||||
|
return _crc32(processed.payload), len(processed.payload), processed.frames
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_mono16k_data(wav: bytes) -> bytes:
|
||||||
|
_require(len(wav) >= 12 and wav[0:4] == b"RIFF" and wav[8:12] == b"WAVE", "KWS source is not RIFF/WAVE")
|
||||||
|
fmt: bytes | None = None
|
||||||
|
data: bytes | None = None
|
||||||
|
offset = 12
|
||||||
|
while offset + 8 <= len(wav):
|
||||||
|
chunk_size = struct.unpack_from("<I", wav, offset + 4)[0]
|
||||||
|
body = wav[offset + 8 : offset + 8 + chunk_size]
|
||||||
|
_require(len(body) == chunk_size, "KWS source chunk is truncated")
|
||||||
|
if wav[offset : offset + 4] == b"fmt ":
|
||||||
|
fmt = body
|
||||||
|
elif wav[offset : offset + 4] == b"data":
|
||||||
|
data = body
|
||||||
|
offset += 8 + chunk_size + (chunk_size & 1)
|
||||||
|
_require(fmt is not None and len(fmt) >= 16 and data is not None, "KWS source lacks fmt/data")
|
||||||
|
audio_format, channels, rate, _byte_rate, _align, bits = struct.unpack_from("<HHIIHH", fmt)
|
||||||
|
_require(
|
||||||
|
(audio_format, channels, rate, bits) == (1, 1, 16000, 16),
|
||||||
|
"KWS source is not mono/16000/S16 PCM",
|
||||||
|
)
|
||||||
|
_require(len(data) <= KWS_WINDOW_BYTES and len(data) % 2 == 0, "KWS source exceeds one window")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _load_kws_pcm(corpus_root: Path) -> tuple[bytes, int]:
|
||||||
|
source = corpus_root / KWS_CORPUS_RELPATH
|
||||||
|
_require(source.is_file(), f"locked KWS corpus utterance missing: {source}")
|
||||||
|
wav = source.read_bytes()
|
||||||
|
_require(_sha256(wav) == KWS_WAV_SHA256, f"KWS source WAV SHA-256 mismatch: {source}")
|
||||||
|
padded = _extract_mono16k_data(wav)
|
||||||
|
padded = padded + bytes(KWS_WINDOW_BYTES - len(padded))
|
||||||
|
_require(_sha256(padded) == KWS_PCM_SHA256, f"KWS padded PCM SHA-256 mismatch: {source}")
|
||||||
|
return padded, _crc32(padded)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_array(name: str, data: bytes) -> str:
|
||||||
|
lines = [f"static const uint8_t {name}[{len(data)}] __attribute__((aligned(64))) = {{"]
|
||||||
|
for offset in range(0, len(data), 16):
|
||||||
|
chunk = ",".join(f"0x{value:02X}" for value in data[offset : offset + 16])
|
||||||
|
lines.append(f" {chunk},")
|
||||||
|
lines.append("};")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _emit(args: argparse.Namespace) -> str:
|
||||||
|
apumc, apumc_crc, apumc_payload_crc = _load_apumc(args.apumc)
|
||||||
|
apum, apum_crc = _load_apum(args.apum)
|
||||||
|
wav = _synthesize_wav()
|
||||||
|
pcm_crc, pcm_bytes, pcm_frames = _expected_pcm(wav)
|
||||||
|
kws_pcm, kws_crc = _load_kws_pcm(args.kws_corpus)
|
||||||
|
|
||||||
|
sections = [
|
||||||
|
"/* Generated by scripts/build_apu_release_assets.py; do not edit. */",
|
||||||
|
"#ifndef APU_RELEASE_ASSETS_H",
|
||||||
|
"#define APU_RELEASE_ASSETS_H",
|
||||||
|
"",
|
||||||
|
"#include <stdint.h>",
|
||||||
|
"",
|
||||||
|
f"#define RS_APU_RELEASE_APUMC_SIZE UINT32_C({len(apumc)})",
|
||||||
|
f"#define RS_APU_RELEASE_APUMC_CRC UINT32_C(0x{apumc_crc:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_APUMC_PAYLOAD_CRC UINT32_C(0x{apumc_payload_crc:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_APUM_SIZE UINT32_C({len(apum)})",
|
||||||
|
f"#define RS_APU_RELEASE_APUM_CRC UINT32_C(0x{apum_crc:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_APUM_PAYLOAD_CRC UINT32_C(0x{APUM_PAYLOAD_CRC:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_WAV_SIZE UINT32_C({len(wav)})",
|
||||||
|
f"#define RS_APU_RELEASE_WAV_CRC UINT32_C(0x{_crc32(wav):08X})",
|
||||||
|
f"#define RS_APU_RELEASE_WAV_RATE UINT32_C({WAV_RATE})",
|
||||||
|
f"#define RS_APU_RELEASE_WAV_CHANNELS UINT32_C({WAV_CHANNELS})",
|
||||||
|
f"#define RS_APU_RELEASE_WAV_BITS UINT32_C({WAV_BITS})",
|
||||||
|
f"#define RS_APU_RELEASE_PCM_CRC UINT32_C(0x{pcm_crc:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_PCM_BYTES UINT32_C({pcm_bytes})",
|
||||||
|
f"#define RS_APU_RELEASE_PCM_FRAMES UINT32_C({pcm_frames})",
|
||||||
|
f"#define RS_APU_RELEASE_KWS_SIZE UINT32_C({len(kws_pcm)})",
|
||||||
|
f"#define RS_APU_RELEASE_KWS_CRC UINT32_C(0x{kws_crc:08X})",
|
||||||
|
f"#define RS_APU_RELEASE_KWS_CLASS UINT32_C({KWS_EXPECTED_CLASS})",
|
||||||
|
f"#define RS_APU_RELEASE_KWS_THRESHOLD UINT32_C({KWS_THRESHOLD})",
|
||||||
|
f"#define RS_APU_RELEASE_KWS_DEBOUNCE UINT32_C({KWS_DEBOUNCE})",
|
||||||
|
"",
|
||||||
|
_format_array("rs_apu_release_apumc", apumc),
|
||||||
|
"",
|
||||||
|
_format_array("rs_apu_release_apum", apum),
|
||||||
|
"",
|
||||||
|
_format_array("rs_apu_release_wav", wav),
|
||||||
|
"",
|
||||||
|
_format_array("rs_apu_release_kws_pcm", kws_pcm),
|
||||||
|
"",
|
||||||
|
"#endif",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
return "\n".join(sections)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--apumc", type=Path, required=True, help="P5 APUMC bundle path")
|
||||||
|
parser.add_argument("--apum", type=Path, required=True, help="P7 APUM KWS model path")
|
||||||
|
parser.add_argument(
|
||||||
|
"--kws-corpus",
|
||||||
|
type=Path,
|
||||||
|
required=True,
|
||||||
|
help="locked apu-kws-pcm corpus root (run the P7 reference setup first)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--output", type=Path, required=True, help="generated C header path")
|
||||||
|
args = parser.parse_args()
|
||||||
|
content = _emit(args)
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(content, encoding="ascii", newline="\n")
|
||||||
|
print(f"apu_release assets: {args.output} ({len(content)} bytes)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the hart-1 APU ownership-evidence payload used by HP RTL simulation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def build(args: argparse.Namespace) -> None:
|
||||||
|
output = args.output.resolve()
|
||||||
|
images = output / "images"
|
||||||
|
images.mkdir(parents=True, exist_ok=True)
|
||||||
|
elf = output / "hp_apu.elf"
|
||||||
|
compiler = f"{args.cross}gcc"
|
||||||
|
objcopy = f"{args.cross}objcopy"
|
||||||
|
includes = [f"-I{path.resolve()}" for path in args.include]
|
||||||
|
source_dir = args.source_dir.resolve()
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
compiler,
|
||||||
|
"-march=rv32imafdc_zicbom_zicsr_zifencei",
|
||||||
|
"-mabi=ilp32d",
|
||||||
|
"-nostdlib",
|
||||||
|
"-nostartfiles",
|
||||||
|
"-ffreestanding",
|
||||||
|
"-O2",
|
||||||
|
"-Wall",
|
||||||
|
"-Wextra",
|
||||||
|
"-Wl,--build-id=none",
|
||||||
|
f"-Wl,-T,{source_dir / 'linker.ld'}",
|
||||||
|
*includes,
|
||||||
|
"-o",
|
||||||
|
str(elf),
|
||||||
|
str(source_dir / "start.S"),
|
||||||
|
str(source_dir / "main.c"),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[objcopy, "-O", "binary", str(elf), str(images / "fw_jump.bin")],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
(images / "retrosoc_hp.dtb").write_bytes(b"SMOK")
|
||||||
|
(images / "Image").write_bytes(b"SMOK")
|
||||||
|
(images / "rootfs.cpio.gz").write_bytes(b"SMOK")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--source-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
parser.add_argument("--cross", default="riscv32-unknown-elf-")
|
||||||
|
parser.add_argument("--include", type=Path, action="append", default=[])
|
||||||
|
build(parser.parse_args())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,856 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the APU-P7 concurrent decode + continuous KWS zero-xrun RTL qualification.
|
||||||
|
|
||||||
|
Builds tests/rtl/apu_p7_concurrent_tb.sv once (sv2v + Icarus), then runs the
|
||||||
|
frozen concurrency matrix: continuous RX KWS concurrent with looped WAV/FLAC
|
||||||
|
decode streamed to I2S TX under the active P5 ready-memory qualification
|
||||||
|
(64-word prefill, then ready memory without injected stalls). Each scenario
|
||||||
|
requires zero codec underrun, zero I2S RX/KWS overrun, every full uninterrupted
|
||||||
|
scheduled KWS window completed before its successor, and matching
|
||||||
|
byte/frame/cycle/stall scoreboards. Writes the concurrent.json report and exits
|
||||||
|
nonzero on any violation or missing input. Nothing is downloaded.
|
||||||
|
|
||||||
|
Prerequisites (no network access is used by this script):
|
||||||
|
- Icarus Verilog (iverilog/vvp) and sv2v on PATH.
|
||||||
|
- The deterministic P5 codec bundle `apu-p5.apumc`, built once with
|
||||||
|
`make CONFIG=configs/ci/ihp130.mk apu-p5-bundle` (writes
|
||||||
|
build/<variant>/apu/p5/apu-p5.apumc). Pass it with --apumc or let the newest
|
||||||
|
build/*/apu/p5/apu-p5.apumc be auto-discovered.
|
||||||
|
- The locked P7 TFLite model under .cache/retrosoc/sources/apu-mlperf-tiny
|
||||||
|
(installed by the shared P7 reference setup); the APUM image is derived
|
||||||
|
locally with scripts/apu_kws_convert.py. --apum accepts a prebuilt image.
|
||||||
|
|
||||||
|
The frozen release gate is 60 simulated seconds per scenario at 48 MHz PCLK
|
||||||
|
(--seconds 60). The default is a short development run; --smoke selects the
|
||||||
|
minimal single-scenario matrix for pytest.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
from apu_codecs import crc8, crc16, decode_flac, decode_wav, process_pcm # noqa: E402
|
||||||
|
from apu_kws import APUM_PAYLOAD_CRC, APUM_SHA256 # noqa: E402
|
||||||
|
|
||||||
|
PCLK_HZ = 48_000_000
|
||||||
|
FULL_GATE_SECONDS = 60.0
|
||||||
|
FULL_GATE_CYCLES = int(FULL_GATE_SECONDS * PCLK_HZ)
|
||||||
|
FLAC_CONSTANT = -2
|
||||||
|
CONTRACT_REVISION = "apu-kws-convert/1.0.0"
|
||||||
|
DEFAULT_TFLITE = (
|
||||||
|
ROOT
|
||||||
|
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
|
||||||
|
/ "trained_models/kws_ref_model.tflite"
|
||||||
|
)
|
||||||
|
BUNDLE_PREREQUISITE = "make CONFIG=configs/ci/ihp130.mk apu-p5-bundle"
|
||||||
|
PROFILE = "configs/ci/ihp130.mk"
|
||||||
|
|
||||||
|
RESULT_PATTERN = re.compile(
|
||||||
|
r"CONCURRENT_RESULT "
|
||||||
|
r"rate=(?P<rate>\d+) "
|
||||||
|
r"precision=(?P<precision>\d+) "
|
||||||
|
r"seconds_ms=(?P<seconds_ms>\d+) "
|
||||||
|
r"duration_cycles=(?P<duration_cycles>\d+) "
|
||||||
|
r"jobs=(?P<jobs>\d+) "
|
||||||
|
r"wav_jobs=(?P<wav_jobs>\d+) "
|
||||||
|
r"flac_jobs=(?P<flac_jobs>\d+) "
|
||||||
|
r"tx_words=(?P<tx_words>\d+) "
|
||||||
|
r"consumed=(?P<consumed>\d+) "
|
||||||
|
r"underrun=(?P<underrun>\d+) "
|
||||||
|
r"rx_overrun=(?P<rx_overrun>\d+) "
|
||||||
|
r"kws_overrun=(?P<kws_overrun>\d+) "
|
||||||
|
r"kws_frames=(?P<kws_frames>\d+) "
|
||||||
|
r"kws_windows=(?P<kws_windows>\d+) "
|
||||||
|
r"rx_words=(?P<rx_words>\d+) "
|
||||||
|
r"codec_input_bytes=(?P<codec_input_bytes>\d+) "
|
||||||
|
r"codec_output_bytes=(?P<codec_output_bytes>\d+) "
|
||||||
|
r"codec_frames=(?P<codec_frames>\d+) "
|
||||||
|
r"codec_cycles=(?P<codec_cycles>\d+) "
|
||||||
|
r"dma_read_stalls=(?P<dma_read_stalls>\d+) "
|
||||||
|
r"dma_write_stalls=(?P<dma_write_stalls>\d+) "
|
||||||
|
r"stream_stalls=(?P<stream_stalls>\d+) "
|
||||||
|
r"active_cycles=(?P<active_cycles>\d+) "
|
||||||
|
r"elapsed_cycles=(?P<elapsed_cycles>\d+) "
|
||||||
|
r"scoreboard=(?P<scoreboard>\w+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_bytes(data: bytes) -> str:
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as source:
|
||||||
|
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _hex_words(path: Path, payload: bytes, words: int) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
lines = []
|
||||||
|
for index in range(words):
|
||||||
|
chunk = payload[index * 4 : index * 4 + 4]
|
||||||
|
lines.append(f"{int.from_bytes(chunk.ljust(4, bytes(1)), 'little'):08x}\n")
|
||||||
|
path.write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_command(command: list[str], *, timeout: int | None = None) -> dict[str, Any]:
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
process = subprocess.run(
|
||||||
|
command, cwd=ROOT, check=False, capture_output=True, text=True, timeout=timeout
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"command": command,
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"stdout": process.stdout,
|
||||||
|
"stderr": process.stderr,
|
||||||
|
"timed_out": False,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired as error:
|
||||||
|
stdout = error.stdout.decode("utf-8", errors="replace") if error.stdout else ""
|
||||||
|
stderr = error.stderr.decode("utf-8", errors="replace") if error.stderr else ""
|
||||||
|
return {
|
||||||
|
"command": command,
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"exit_code": 124,
|
||||||
|
"stdout": stdout,
|
||||||
|
"stderr": stderr,
|
||||||
|
"timed_out": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_version(name: str, flag: str = "--version") -> dict[str, Any]:
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path is None:
|
||||||
|
return {"path": None, "version": None}
|
||||||
|
probe = _run_command([path, flag], timeout=30)
|
||||||
|
first_line = (probe["stdout"] or probe["stderr"]).splitlines()
|
||||||
|
return {
|
||||||
|
"path": path,
|
||||||
|
"version": first_line[0].strip() if first_line else "unknown",
|
||||||
|
"sha256": _sha256_file(Path(path)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _git_revision() -> tuple[str, bool]:
|
||||||
|
revision = _run_command(["git", "rev-parse", "HEAD"], timeout=30)
|
||||||
|
dirty = _run_command(["git", "status", "--porcelain"], timeout=30)
|
||||||
|
return (
|
||||||
|
revision["stdout"].strip() if revision["exit_code"] == 0 else "unknown",
|
||||||
|
bool(dirty["stdout"].strip()) if dirty["exit_code"] == 0 else True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _production_sources() -> list[Path]:
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
names = (
|
||||||
|
"apu_microcode_pkg.sv",
|
||||||
|
"apu_dma.sv",
|
||||||
|
"apu_ring_scheduler.sv",
|
||||||
|
"apu_stream_router.sv",
|
||||||
|
"apu_control_store.sv",
|
||||||
|
"apu_microcode_loader.sv",
|
||||||
|
"apu_local_sram.sv",
|
||||||
|
"apu_kws_engine.sv",
|
||||||
|
"apu_kws_sram_client.sv",
|
||||||
|
"apu_kws_model_loader.sv",
|
||||||
|
"apu_bitstream_engine.sv",
|
||||||
|
"apu_entropy_engine.sv",
|
||||||
|
"apu_reconstruction_engine.sv",
|
||||||
|
"apu_transform_engine.sv",
|
||||||
|
"apu_resampler.sv",
|
||||||
|
"apu_kernel_engine.sv",
|
||||||
|
"apu_primitive_dispatcher.sv",
|
||||||
|
"apu_codec_sequencer.sv",
|
||||||
|
"apu_codec_transport.sv",
|
||||||
|
"apu_codec_controller.sv",
|
||||||
|
"apu_reg.sv",
|
||||||
|
"apb4_apu.sv",
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
common / "interface/apb4_if.sv",
|
||||||
|
common / "interface/axi4_if.sv",
|
||||||
|
common / "interface/axi4_stream_if.sv",
|
||||||
|
common / "utils/register.sv",
|
||||||
|
common / "utils/fifo.sv",
|
||||||
|
ROOT / "rtl/tech/tc_sram.sv",
|
||||||
|
ROOT / "rtl/ip/peripheral/dma_axi4_master.sv",
|
||||||
|
*(multimedia / name for name in names),
|
||||||
|
ROOT / "tests/rtl/apu_p7_concurrent_tb.sv",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _verification_sources() -> dict[str, str]:
|
||||||
|
inputs = [
|
||||||
|
*_production_sources(),
|
||||||
|
ROOT / "rtl/ip/multimedia/apu_define.svh",
|
||||||
|
ROOT / "scripts/apu_codecs.py",
|
||||||
|
ROOT / "scripts/apu_kws.py",
|
||||||
|
ROOT / "scripts/apu_kws_convert.py",
|
||||||
|
ROOT / "rtl/mini/script/convt_sv2v.py",
|
||||||
|
Path(__file__).resolve(),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
path.relative_to(ROOT).as_posix(): _sha256_file(path)
|
||||||
|
for path in sorted(inputs)
|
||||||
|
if path.is_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_apumc(apumc: Path | None) -> Path:
|
||||||
|
if apumc is not None:
|
||||||
|
resolved = apumc.resolve()
|
||||||
|
if not resolved.is_file():
|
||||||
|
raise SystemExit(f"APUMC bundle not found: {resolved}\nprerequisite: {BUNDLE_PREREQUISITE}")
|
||||||
|
return resolved
|
||||||
|
candidates = sorted(
|
||||||
|
ROOT.glob("build/*/apu/p5/apu-p5.apumc"), key=lambda path: path.stat().st_mtime
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
raise SystemExit(
|
||||||
|
"no APUMC bundle found below build/*/apu/p5/\n"
|
||||||
|
f"prerequisite: {BUNDLE_PREREQUISITE}"
|
||||||
|
)
|
||||||
|
return candidates[-1].resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_apum(apum: Path | None) -> tuple[bytes, str]:
|
||||||
|
if apum is not None:
|
||||||
|
resolved = apum.resolve()
|
||||||
|
if not resolved.is_file():
|
||||||
|
raise SystemExit(f"APUM model image not found: {resolved}")
|
||||||
|
payload = resolved.read_bytes()
|
||||||
|
return payload, str(resolved)
|
||||||
|
if not DEFAULT_TFLITE.is_file():
|
||||||
|
raise SystemExit(
|
||||||
|
f"locked P7 TFLite model not installed: {DEFAULT_TFLITE}\n"
|
||||||
|
"prerequisite: make CONFIG=configs/ci/ihp130.mk setup-apu-kws-reference"
|
||||||
|
)
|
||||||
|
from apu_kws_convert import import_tflite
|
||||||
|
|
||||||
|
payload = import_tflite(DEFAULT_TFLITE)
|
||||||
|
return payload, f"generated:{DEFAULT_TFLITE}"
|
||||||
|
|
||||||
|
|
||||||
|
def _wav_payload(rate: int, bits: int, frames: int, seed: int) -> bytes:
|
||||||
|
block_align = 2 * (bits // 8)
|
||||||
|
state = seed | 1
|
||||||
|
data = bytearray()
|
||||||
|
for frame in range(frames):
|
||||||
|
for channel in range(2):
|
||||||
|
state = (1103515245 * state + 12345 + channel + frame) & 0x7FFFFFFF
|
||||||
|
value = ((state >> 8) & ((1 << bits) - 1)) - (1 << (bits - 1))
|
||||||
|
data.extend(value.to_bytes(bits // 8, "little", signed=True))
|
||||||
|
header = bytearray()
|
||||||
|
header.extend(b"RIFF")
|
||||||
|
header.extend((36 + len(data)).to_bytes(4, "little"))
|
||||||
|
header.extend(b"WAVE")
|
||||||
|
header.extend(b"fmt ")
|
||||||
|
header.extend((16).to_bytes(4, "little"))
|
||||||
|
header.extend((1).to_bytes(2, "little"))
|
||||||
|
header.extend((2).to_bytes(2, "little"))
|
||||||
|
header.extend(rate.to_bytes(4, "little"))
|
||||||
|
header.extend((rate * block_align).to_bytes(4, "little"))
|
||||||
|
header.extend(block_align.to_bytes(2, "little"))
|
||||||
|
header.extend(bits.to_bytes(2, "little"))
|
||||||
|
header.extend(b"data")
|
||||||
|
header.extend(len(data).to_bytes(4, "little"))
|
||||||
|
return bytes(header + data)
|
||||||
|
|
||||||
|
|
||||||
|
def _flac_payload(rate: int, bits: int, block: int) -> bytes:
|
||||||
|
streaminfo = bytearray(34)
|
||||||
|
streaminfo[0:2] = block.to_bytes(2, "big")
|
||||||
|
streaminfo[2:4] = block.to_bytes(2, "big")
|
||||||
|
streaminfo[10:18] = ((rate << 44) | ((bits - 1) << 36) | block).to_bytes(8, "big")
|
||||||
|
size_code = 4 if bits == 16 else 6
|
||||||
|
frame_header = ((0x3FFE << 18) | (7 << 12) | (size_code << 1)).to_bytes(4, "big")
|
||||||
|
frame_header += b"\x00" + (block - 1).to_bytes(2, "big")
|
||||||
|
frame_header += bytes([crc8(frame_header)])
|
||||||
|
frame = frame_header + b"\x00" + FLAC_CONSTANT.to_bytes(bits // 8, "big", signed=True)
|
||||||
|
frame += crc16(frame).to_bytes(2, "big")
|
||||||
|
return b"fLaC" + bytes([0x80, 0, 0, 34]) + bytes(streaminfo) + frame
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_pcm(
|
||||||
|
source: bytes, *, is_wav: bool, rate: int, bits: int, resample: bool
|
||||||
|
) -> tuple[list[int], int]:
|
||||||
|
decoded = decode_wav(source, strict=True) if is_wav else decode_flac(source, strict=True)
|
||||||
|
processed = process_pcm(
|
||||||
|
decoded, output_rate=rate, output_channels=2, output_bits=bits, resample=resample,
|
||||||
|
i2s=True,
|
||||||
|
)
|
||||||
|
payload = processed.payload
|
||||||
|
if len(payload) % 4 != 0:
|
||||||
|
raise RuntimeError("BAM PCM payload is not word aligned")
|
||||||
|
words = [
|
||||||
|
int.from_bytes(payload[offset : offset + 4], "little")
|
||||||
|
for offset in range(0, len(payload), 4)
|
||||||
|
]
|
||||||
|
return words, processed.frames
|
||||||
|
|
||||||
|
|
||||||
|
def _write_pattern_hex(path: Path, words: list[int]) -> None:
|
||||||
|
padded = list(words) + [0] * (16384 - len(words))
|
||||||
|
path.write_text("".join(f"{word:08x}\n" for word in padded), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _compile(build_dir: Path) -> dict[str, Any]:
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
filelist = build_dir / "apu_p7_concurrent.fl"
|
||||||
|
filelist.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"+define+SV_ASSRT_DISABLE",
|
||||||
|
f"+incdir+{common}",
|
||||||
|
f"+incdir+{common / 'interface'}",
|
||||||
|
f"+incdir+{multimedia}",
|
||||||
|
*(str(source) for source in _production_sources()),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
converted = build_dir / "apu_p7_concurrent.v"
|
||||||
|
conversion = _run_command(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||||
|
"-f",
|
||||||
|
str(filelist),
|
||||||
|
"--output",
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
timeout=900,
|
||||||
|
)
|
||||||
|
if conversion["exit_code"] != 0:
|
||||||
|
raise RuntimeError(f"sv2v conversion failed: {conversion['stderr'][-2000:]}")
|
||||||
|
binary = build_dir / "apu_p7_concurrent_sim"
|
||||||
|
icarus = _run_command(
|
||||||
|
[
|
||||||
|
"iverilog",
|
||||||
|
"-g2012",
|
||||||
|
"-s",
|
||||||
|
"apu_p7_concurrent_tb",
|
||||||
|
"-o",
|
||||||
|
str(binary),
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
timeout=900,
|
||||||
|
)
|
||||||
|
if icarus["exit_code"] != 0:
|
||||||
|
raise RuntimeError(f"Icarus compile failed: {icarus['stderr'][-2000:]}")
|
||||||
|
result = {
|
||||||
|
"status": "passed",
|
||||||
|
"filelist": str(filelist),
|
||||||
|
"converted": str(converted),
|
||||||
|
"converted_sha256": _sha256_file(converted),
|
||||||
|
"binary": str(binary),
|
||||||
|
"sv2v": conversion,
|
||||||
|
"icarus": icarus,
|
||||||
|
}
|
||||||
|
_write_json(build_dir / "compile.json", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _kws_schedule(rx_words: int, precision: int, rate: int) -> tuple[int, int, int]:
|
||||||
|
words_per_frame = 2 if precision == 24 else 1
|
||||||
|
decimation = 6 if rate == 96000 else 3
|
||||||
|
samples_16k = (rx_words // words_per_frame) // decimation
|
||||||
|
frames = (samples_16k - 480) // 320 + 1 if samples_16k >= 480 else 0
|
||||||
|
windows = (samples_16k - 16000) // 1600 + 1 if samples_16k >= 16000 else 0
|
||||||
|
return samples_16k, frames, windows
|
||||||
|
|
||||||
|
|
||||||
|
def _check_scenario(
|
||||||
|
scenario: dict[str, Any], parsed: dict[str, Any], workloads: dict[str, Any]
|
||||||
|
) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
rate = scenario["rate_hz"]
|
||||||
|
precision = scenario["precision_bits"]
|
||||||
|
if parsed["scoreboard"] != "match":
|
||||||
|
errors.append("testbench scoreboard did not match")
|
||||||
|
for field in ("underrun", "rx_overrun", "kws_overrun"):
|
||||||
|
if parsed[field] != 0:
|
||||||
|
errors.append(f"{field}={parsed[field]} violates the zero-xrun gate")
|
||||||
|
expected_rx_words = scenario["duration_cycles"] // scenario["frame_period"]
|
||||||
|
if parsed["rx_words"] != expected_rx_words:
|
||||||
|
errors.append(f"rx_words={parsed['rx_words']} != expected {expected_rx_words}")
|
||||||
|
samples_16k, kws_frames, kws_windows = _kws_schedule(parsed["rx_words"], precision, rate)
|
||||||
|
scenario["kws_16k_samples"] = samples_16k
|
||||||
|
scenario["expected_kws_frames"] = kws_frames
|
||||||
|
scenario["expected_kws_windows"] = kws_windows
|
||||||
|
if parsed["kws_frames"] != kws_frames:
|
||||||
|
errors.append(f"kws_frames={parsed['kws_frames']} != expected {kws_frames}")
|
||||||
|
if parsed["kws_windows"] != kws_windows:
|
||||||
|
errors.append(f"kws_windows={parsed['kws_windows']} != expected {kws_windows}")
|
||||||
|
if parsed["jobs"] != parsed["wav_jobs"] + parsed["flac_jobs"]:
|
||||||
|
errors.append("job accounting mismatch")
|
||||||
|
if parsed["wav_jobs"] < 1 or parsed["flac_jobs"] < 1:
|
||||||
|
errors.append("WAV and FLAC workloads must both run in every scenario")
|
||||||
|
expected_tx = (
|
||||||
|
parsed["wav_jobs"] * workloads["wav_out_words"]
|
||||||
|
+ parsed["flac_jobs"] * workloads["flac_out_words"]
|
||||||
|
)
|
||||||
|
if parsed["tx_words"] != expected_tx or parsed["consumed"] != expected_tx:
|
||||||
|
errors.append(f"tx_words/consumed != expected {expected_tx}")
|
||||||
|
expected_input = (
|
||||||
|
parsed["wav_jobs"] * workloads["wav_bytes"]
|
||||||
|
+ parsed["flac_jobs"] * workloads["flac_bytes"]
|
||||||
|
)
|
||||||
|
if parsed["codec_input_bytes"] != expected_input:
|
||||||
|
errors.append(f"codec_input_bytes != expected {expected_input}")
|
||||||
|
if parsed["codec_output_bytes"] != expected_tx * 4:
|
||||||
|
errors.append(f"codec_output_bytes != expected {expected_tx * 4}")
|
||||||
|
expected_frames = (
|
||||||
|
parsed["wav_jobs"] * workloads["wav_frames"]
|
||||||
|
+ parsed["flac_jobs"] * workloads["flac_frames"]
|
||||||
|
)
|
||||||
|
if parsed["codec_frames"] != expected_frames:
|
||||||
|
errors.append(f"codec_frames != expected {expected_frames}")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _run_scenario(
|
||||||
|
scenario: dict[str, Any],
|
||||||
|
compile_result: dict[str, Any],
|
||||||
|
artifacts: dict[str, str],
|
||||||
|
workloads: dict[str, Any],
|
||||||
|
timeout: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
scenario_dir = Path(artifacts["scenario_dir"])
|
||||||
|
log_path = scenario_dir / "run.log"
|
||||||
|
command = [
|
||||||
|
"vvp",
|
||||||
|
compile_result["binary"],
|
||||||
|
f"+APUMC_HEX={artifacts['apumc_hex']}",
|
||||||
|
f"+APUM_HEX={artifacts['apum_hex']}",
|
||||||
|
f"+WAV_HEX={scenario_dir / 'wav.hex'}",
|
||||||
|
f"+FLAC_HEX={scenario_dir / 'flac.hex'}",
|
||||||
|
f"+WAV_PCM_HEX={scenario_dir / 'wav_pcm.hex'}",
|
||||||
|
f"+FLAC_PCM_HEX={scenario_dir / 'flac_pcm.hex'}",
|
||||||
|
f"+WAV_BYTES={workloads['wav_bytes']}",
|
||||||
|
f"+WAV_FRAMES={workloads['wav_frames']}",
|
||||||
|
f"+WAV_OUT_WORDS={workloads['wav_out_words']}",
|
||||||
|
f"+WAV_SOURCE_RATE={workloads['wav_source_rate']}",
|
||||||
|
f"+FLAC_BYTES={workloads['flac_bytes']}",
|
||||||
|
f"+FLAC_FRAMES={workloads['flac_frames']}",
|
||||||
|
f"+FLAC_OUT_WORDS={workloads['flac_out_words']}",
|
||||||
|
f"+RATE={scenario['rate_hz']}",
|
||||||
|
f"+PRECISION={scenario['precision_bits']}",
|
||||||
|
f"+DURATION_CYCLES={scenario['duration_cycles']}",
|
||||||
|
f"+SECONDS_MS={scenario['seconds_ms']}",
|
||||||
|
f"+MAX_CYCLES={scenario['max_cycles']}",
|
||||||
|
f"+SEED={scenario['seed']}",
|
||||||
|
"+WORKLOAD_MASK=3",
|
||||||
|
]
|
||||||
|
execution = _run_command(command, timeout=timeout)
|
||||||
|
log_path.write_text(execution["stdout"] + execution["stderr"], encoding="utf-8")
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"scenario": scenario["id"],
|
||||||
|
"command": command,
|
||||||
|
"log": str(log_path),
|
||||||
|
"exit_code": execution["exit_code"],
|
||||||
|
"timed_out": execution["timed_out"],
|
||||||
|
"wall_seconds": execution["duration_seconds"],
|
||||||
|
"timeout_seconds": timeout,
|
||||||
|
"status": "failed",
|
||||||
|
"errors": [],
|
||||||
|
}
|
||||||
|
match = RESULT_PATTERN.search(execution["stdout"])
|
||||||
|
if execution["exit_code"] != 0 or match is None:
|
||||||
|
tail = (execution["stdout"] + execution["stderr"]).strip().splitlines()[-8:]
|
||||||
|
result["errors"].append(
|
||||||
|
"simulator failed" if execution["exit_code"] != 0 else "no CONCURRENT_RESULT line"
|
||||||
|
)
|
||||||
|
result["log_tail"] = tail
|
||||||
|
return result
|
||||||
|
parsed: dict[str, Any] = {
|
||||||
|
name: int(value) for name, value in match.groupdict().items() if name != "scoreboard"
|
||||||
|
}
|
||||||
|
parsed["scoreboard"] = match.group("scoreboard")
|
||||||
|
result["result"] = parsed
|
||||||
|
result["simulated_cycles_per_wall_second"] = (
|
||||||
|
round(parsed["elapsed_cycles"] / execution["duration_seconds"], 3)
|
||||||
|
if execution["duration_seconds"] > 0
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
errors = _check_scenario(scenario, parsed, workloads)
|
||||||
|
result["errors"] = errors
|
||||||
|
result["status"] = "passed" if not errors else "failed"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--build-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--apumc", type=Path, default=None)
|
||||||
|
parser.add_argument("--apum", type=Path, default=None)
|
||||||
|
parser.add_argument(
|
||||||
|
"--seconds",
|
||||||
|
type=float,
|
||||||
|
default=0.5,
|
||||||
|
help="simulated seconds per scenario; the frozen release gate is 60",
|
||||||
|
)
|
||||||
|
parser.add_argument("--smoke", action="store_true", help="minimal single-scenario matrix")
|
||||||
|
parser.add_argument(
|
||||||
|
"--rates",
|
||||||
|
type=int,
|
||||||
|
nargs="+",
|
||||||
|
default=None,
|
||||||
|
choices=(48000, 96000),
|
||||||
|
help="I2S rates to run (default both, or 48000 in --smoke)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--precisions",
|
||||||
|
type=int,
|
||||||
|
nargs="+",
|
||||||
|
default=None,
|
||||||
|
choices=(16, 24),
|
||||||
|
help="output precisions to run (default both, or 16 in --smoke)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--smoke-seconds",
|
||||||
|
type=float,
|
||||||
|
default=0.03,
|
||||||
|
help="simulated seconds per scenario in --smoke mode (kept short for pytest)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--job-frames",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="stereo frames per WAV/FLAC job (default 512 in --smoke, else 4096)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--timeout-seconds", type=int, default=14400)
|
||||||
|
parser.add_argument("--seed", type=int, default=0)
|
||||||
|
parser.add_argument("--output", type=Path, default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
build_dir = args.build_dir.resolve()
|
||||||
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output = args.output.resolve() if args.output else build_dir / "concurrent.json"
|
||||||
|
|
||||||
|
for tool in ("iverilog", "vvp", "sv2v"):
|
||||||
|
if shutil.which(tool) is None:
|
||||||
|
raise SystemExit(f"required tool not on PATH: {tool}")
|
||||||
|
if args.seconds <= 0 or args.smoke_seconds <= 0:
|
||||||
|
raise SystemExit("--seconds/--smoke-seconds must be positive")
|
||||||
|
seconds = args.smoke_seconds if args.smoke else args.seconds
|
||||||
|
job_frames = args.job_frames
|
||||||
|
if job_frames is None:
|
||||||
|
job_frames = 512 if args.smoke else 4096
|
||||||
|
if job_frames < 64 or job_frames > 4096:
|
||||||
|
raise SystemExit("--job-frames must be within 64..4096")
|
||||||
|
apumc_path = _resolve_apumc(args.apumc)
|
||||||
|
apumc_payload = apumc_path.read_bytes()
|
||||||
|
apum_payload, apum_source = _resolve_apum(args.apum)
|
||||||
|
if len(apum_payload) != 32768:
|
||||||
|
raise SystemExit(f"APUM image must be 32768 bytes, got {len(apum_payload)}")
|
||||||
|
apum_sha256 = _sha256_bytes(apum_payload)
|
||||||
|
apum_matches_freeze = apum_sha256 == APUM_SHA256
|
||||||
|
|
||||||
|
if args.smoke and (args.rates is not None or args.precisions is not None):
|
||||||
|
raise SystemExit("--smoke selects its own minimal matrix; do not combine with --rates/--precisions")
|
||||||
|
rates = tuple(args.rates) if args.rates is not None else ((48000,) if args.smoke else (48000, 96000))
|
||||||
|
precisions = (
|
||||||
|
tuple(args.precisions) if args.precisions is not None else ((16,) if args.smoke else (16, 24))
|
||||||
|
)
|
||||||
|
scenarios: list[dict[str, Any]] = []
|
||||||
|
for rate in rates:
|
||||||
|
for precision in precisions:
|
||||||
|
precision_id = "s16" if precision == 16 else "s24_32"
|
||||||
|
duration_cycles = int(round(seconds * PCLK_HZ))
|
||||||
|
frame_period = PCLK_HZ // rate // (2 if precision == 24 else 1)
|
||||||
|
if duration_cycles % frame_period != 0:
|
||||||
|
raise SystemExit(
|
||||||
|
f"--seconds {seconds} does not divide the {rate}-Hz/{precision}-bit "
|
||||||
|
"I2S frame cadence; choose a duration with millisecond resolution"
|
||||||
|
)
|
||||||
|
# WAV workload: the P5-qualified playback shapes. A 44.1 kHz
|
||||||
|
# stereo S16 source resampled to 48 kHz for the 48 kHz I2S
|
||||||
|
# scenarios, and an 8 kHz stereo S16 source resampled to 96 kHz
|
||||||
|
# for the 96 kHz scenarios (the PLAYBACK_44100_48000 /
|
||||||
|
# PLAYBACK_8000_96000 qualification shapes). Source frame counts
|
||||||
|
# are multiples of 147 for the exact 160/147 output ratio.
|
||||||
|
wav_source_rate = 44100 if rate == 48000 else 8000
|
||||||
|
if wav_source_rate == 44100:
|
||||||
|
wav_source_frames = max(147, 147 * (job_frames // 147))
|
||||||
|
else:
|
||||||
|
wav_source_frames = job_frames
|
||||||
|
wav = _wav_payload(wav_source_rate, 16, wav_source_frames, args.seed)
|
||||||
|
flac = _flac_payload(rate, precision, job_frames)
|
||||||
|
wav_pcm, wav_frames = _expected_pcm(
|
||||||
|
wav, is_wav=True, rate=rate, bits=precision, resample=True
|
||||||
|
)
|
||||||
|
flac_pcm, flac_frames = _expected_pcm(
|
||||||
|
flac, is_wav=False, rate=rate, bits=precision, resample=False
|
||||||
|
)
|
||||||
|
if flac_frames != job_frames:
|
||||||
|
raise SystemExit("FLAC BAM frame count mismatch")
|
||||||
|
if len(wav_pcm) != wav_frames * (2 if precision == 24 else 1):
|
||||||
|
raise SystemExit("WAV BAM PCM word count mismatch")
|
||||||
|
if len(flac_pcm) != flac_frames * (2 if precision == 24 else 1):
|
||||||
|
raise SystemExit("FLAC BAM PCM word count mismatch")
|
||||||
|
pair_cycles = (wav_frames + flac_frames) * (PCLK_HZ // rate)
|
||||||
|
if duration_cycles < pair_cycles:
|
||||||
|
raise SystemExit(
|
||||||
|
f"--seconds {seconds} at {rate}-Hz/{precision}-bit cannot fit one "
|
||||||
|
f"WAV+FLAC job pair ({pair_cycles} playback cycles); increase "
|
||||||
|
"--seconds or reduce --job-frames"
|
||||||
|
)
|
||||||
|
scenarios.append(
|
||||||
|
{
|
||||||
|
"id": f"{rate}-{precision_id}",
|
||||||
|
"rate_hz": rate,
|
||||||
|
"precision_bits": precision,
|
||||||
|
"seconds": seconds,
|
||||||
|
"seconds_ms": int(round(seconds * 1000)),
|
||||||
|
"duration_cycles": duration_cycles,
|
||||||
|
"max_cycles": duration_cycles + 400_000_000,
|
||||||
|
"frame_period": frame_period,
|
||||||
|
"seed": args.seed,
|
||||||
|
"wav": wav,
|
||||||
|
"flac": flac,
|
||||||
|
"wav_pcm": wav_pcm,
|
||||||
|
"flac_pcm": flac_pcm,
|
||||||
|
"workloads": {
|
||||||
|
"wav_bytes": len(wav),
|
||||||
|
"flac_bytes": len(flac),
|
||||||
|
"wav_sha256": _sha256_bytes(wav),
|
||||||
|
"flac_sha256": _sha256_bytes(flac),
|
||||||
|
"wav_source_rate": wav_source_rate,
|
||||||
|
"wav_source_frames": wav_source_frames,
|
||||||
|
"wav_frames": wav_frames,
|
||||||
|
"flac_frames": flac_frames,
|
||||||
|
"wav_out_words": len(wav_pcm),
|
||||||
|
"flac_out_words": len(flac_pcm),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
apumc_hex = build_dir / "apu-p5.hex"
|
||||||
|
_hex_words(apumc_hex, apumc_payload, 16384)
|
||||||
|
apum_hex = build_dir / "kws-apum.hex"
|
||||||
|
_hex_words(apum_hex, apum_payload, 8192)
|
||||||
|
artifacts = {"apumc_hex": str(apumc_hex), "apum_hex": str(apum_hex), "scenario_dir": ""}
|
||||||
|
|
||||||
|
started = time.monotonic()
|
||||||
|
compile_result = _compile(build_dir)
|
||||||
|
|
||||||
|
scenario_results: list[dict[str, Any]] = []
|
||||||
|
failures: list[str] = []
|
||||||
|
for scenario in scenarios:
|
||||||
|
scenario_dir = build_dir / "scenarios" / scenario["id"]
|
||||||
|
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
_hex_words(scenario_dir / "wav.hex", scenario["wav"], 8192)
|
||||||
|
_hex_words(scenario_dir / "flac.hex", scenario["flac"], 1024)
|
||||||
|
_write_pattern_hex(scenario_dir / "wav_pcm.hex", scenario["wav_pcm"])
|
||||||
|
_write_pattern_hex(scenario_dir / "flac_pcm.hex", scenario["flac_pcm"])
|
||||||
|
workloads = scenario["workloads"]
|
||||||
|
artifacts["scenario_dir"] = str(scenario_dir)
|
||||||
|
scenario_results.append(
|
||||||
|
_run_scenario(scenario, compile_result, artifacts, workloads, args.timeout_seconds)
|
||||||
|
)
|
||||||
|
status = scenario_results[-1]["status"]
|
||||||
|
print(f"APU-P7 concurrent: {scenario['id']} {status}", flush=True)
|
||||||
|
_write_json(scenario_dir / "result.json", scenario_results[-1])
|
||||||
|
if status != "passed":
|
||||||
|
failures.extend(f"{scenario['id']}: {error}" for error in scenario_results[-1]["errors"])
|
||||||
|
|
||||||
|
git_revision, git_dirty = _git_revision()
|
||||||
|
cycles_per_second = [
|
||||||
|
item["simulated_cycles_per_wall_second"]
|
||||||
|
for item in scenario_results
|
||||||
|
if item.get("simulated_cycles_per_wall_second")
|
||||||
|
]
|
||||||
|
slowest = min(cycles_per_second) if cycles_per_second else None
|
||||||
|
projected_per_config = (
|
||||||
|
round(FULL_GATE_CYCLES / slowest, 1) if slowest else None
|
||||||
|
)
|
||||||
|
full_matrix_configs = 4
|
||||||
|
projection = {
|
||||||
|
"full_gate_seconds_per_config": FULL_GATE_SECONDS,
|
||||||
|
"full_gate_cycles_per_config": FULL_GATE_CYCLES,
|
||||||
|
"full_matrix_configs": full_matrix_configs,
|
||||||
|
"measured_min_cycles_per_wall_second": slowest,
|
||||||
|
"projected_wall_seconds_per_config": projected_per_config,
|
||||||
|
"projected_wall_seconds_full_matrix": (
|
||||||
|
round(projected_per_config * full_matrix_configs, 1) if projected_per_config else None
|
||||||
|
),
|
||||||
|
"projected_full_matrix_exceeds_12h": (
|
||||||
|
bool(projected_per_config * full_matrix_configs > 12 * 3600)
|
||||||
|
if projected_per_config
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
aggregate = {
|
||||||
|
"scenarios": len(scenario_results),
|
||||||
|
"jobs": sum(item["result"]["jobs"] for item in scenario_results if "result" in item),
|
||||||
|
"codec_underruns": sum(
|
||||||
|
item["result"]["underrun"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"rx_overruns": sum(
|
||||||
|
item["result"]["rx_overrun"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"kws_overruns": sum(
|
||||||
|
item["result"]["kws_overrun"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"codec_input_bytes": sum(
|
||||||
|
item["result"]["codec_input_bytes"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"codec_output_bytes": sum(
|
||||||
|
item["result"]["codec_output_bytes"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"codec_frames": sum(
|
||||||
|
item["result"]["codec_frames"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
"codec_cycles": sum(
|
||||||
|
item["result"]["codec_cycles"] for item in scenario_results if "result" in item
|
||||||
|
),
|
||||||
|
}
|
||||||
|
report = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"phase": "APU-P7",
|
||||||
|
"report": "concurrent",
|
||||||
|
"gate": "concurrent WAV/FLAC decode with continuous RX KWS zero-xrun",
|
||||||
|
"status": "passed" if not failures else "failed",
|
||||||
|
"success": not failures,
|
||||||
|
"release_qualifying": bool(
|
||||||
|
not failures
|
||||||
|
and not args.smoke
|
||||||
|
and seconds >= FULL_GATE_SECONDS
|
||||||
|
and set(rates) == {48000, 96000}
|
||||||
|
and set(precisions) == {16, 24}
|
||||||
|
),
|
||||||
|
"mode": "smoke" if args.smoke else "full-matrix",
|
||||||
|
"seconds_per_scenario": seconds,
|
||||||
|
"job_frames_per_workload": job_frames,
|
||||||
|
"required_full_gate_seconds": FULL_GATE_SECONDS,
|
||||||
|
"pclk_hz": PCLK_HZ,
|
||||||
|
"git_revision": git_revision,
|
||||||
|
"git_dirty": git_dirty,
|
||||||
|
"command": [sys.executable, str(Path(__file__).resolve()), *sys.argv[1:]],
|
||||||
|
"seed": args.seed,
|
||||||
|
"profile": PROFILE,
|
||||||
|
"bundle_prerequisite": BUNDLE_PREREQUISITE,
|
||||||
|
"inputs": {
|
||||||
|
"apumc": {
|
||||||
|
"path": str(apumc_path),
|
||||||
|
"bytes": len(apumc_payload),
|
||||||
|
"sha256": _sha256_bytes(apumc_payload),
|
||||||
|
},
|
||||||
|
"apum": {
|
||||||
|
"path": apum_source,
|
||||||
|
"bytes": len(apum_payload),
|
||||||
|
"sha256": apum_sha256,
|
||||||
|
"payload_crc32": f"0x{APUM_PAYLOAD_CRC:08x}",
|
||||||
|
"matches_locked_freeze": apum_matches_freeze,
|
||||||
|
},
|
||||||
|
"tflite": {
|
||||||
|
"path": str(DEFAULT_TFLITE),
|
||||||
|
"sha256": _sha256_file(DEFAULT_TFLITE) if DEFAULT_TFLITE.is_file() else None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"converter": {
|
||||||
|
"path": "scripts/apu_kws_convert.py",
|
||||||
|
"sha256": _sha256_file(ROOT / "scripts/apu_kws_convert.py"),
|
||||||
|
"contract_revision": CONTRACT_REVISION,
|
||||||
|
},
|
||||||
|
"bam": {
|
||||||
|
"path": "scripts/apu_codecs.py",
|
||||||
|
"sha256": _sha256_file(ROOT / "scripts/apu_codecs.py"),
|
||||||
|
},
|
||||||
|
"rtl_revision": git_revision,
|
||||||
|
"verification_sources": _verification_sources(),
|
||||||
|
"simulator": {
|
||||||
|
"name": "icarus",
|
||||||
|
"iverilog": _tool_version("iverilog", "-V"),
|
||||||
|
"vvp": _tool_version("vvp", "-V"),
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"sv2v": _tool_version("sv2v", "--version"),
|
||||||
|
"python": platform.python_version(),
|
||||||
|
},
|
||||||
|
"compile": compile_result,
|
||||||
|
"harness": {
|
||||||
|
"axi_model": "ready memory, no injected stalls",
|
||||||
|
"playback_prefill_words": 64,
|
||||||
|
"playback_fifo_words": 1024,
|
||||||
|
"router_fifo_words": 64,
|
||||||
|
"rx_model": "one I2S frame word per cadence tick, never re-presented",
|
||||||
|
"kws_config": "0x0180 (threshold 128, debounce 1)",
|
||||||
|
},
|
||||||
|
"matrix": [
|
||||||
|
{
|
||||||
|
"scenario": scenario["id"],
|
||||||
|
"rate_hz": scenario["rate_hz"],
|
||||||
|
"precision_bits": scenario["precision_bits"],
|
||||||
|
"seconds": scenario["seconds"],
|
||||||
|
"workloads": {
|
||||||
|
"wav": {
|
||||||
|
"shape": "resample" if scenario["workloads"]["wav_source_rate"]
|
||||||
|
!= scenario["rate_hz"] else "passthrough",
|
||||||
|
"source_rate_hz": scenario["workloads"]["wav_source_rate"],
|
||||||
|
"source_precision_bits": 16,
|
||||||
|
"source_channels": 2,
|
||||||
|
"source_frames_per_job": scenario["workloads"]["wav_source_frames"],
|
||||||
|
"output_frames_per_job": scenario["workloads"]["wav_frames"],
|
||||||
|
"sha256": scenario["workloads"]["wav_sha256"],
|
||||||
|
},
|
||||||
|
"flac": {
|
||||||
|
"shape": "mono-constant",
|
||||||
|
"rate_hz": scenario["rate_hz"],
|
||||||
|
"precision_bits": scenario["precision_bits"],
|
||||||
|
"block_frames": scenario["workloads"]["flac_frames"],
|
||||||
|
"output_frames_per_job": scenario["workloads"]["flac_frames"],
|
||||||
|
"sha256": scenario["workloads"]["flac_sha256"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for scenario in scenarios
|
||||||
|
],
|
||||||
|
"scenarios": scenario_results,
|
||||||
|
"aggregate_counts": aggregate,
|
||||||
|
"full_gate_projection": projection,
|
||||||
|
"artifacts": {
|
||||||
|
"build_dir": str(build_dir),
|
||||||
|
"apumc_hex": str(apumc_hex),
|
||||||
|
"apum_hex": str(apum_hex),
|
||||||
|
"report": str(output),
|
||||||
|
},
|
||||||
|
"errors": failures,
|
||||||
|
"wall_seconds": round(time.monotonic() - started, 3),
|
||||||
|
}
|
||||||
|
_write_json(output, report)
|
||||||
|
print(f"APU-P7 concurrent report: {output} ({'passed' if not failures else 'failed'})")
|
||||||
|
return 0 if not failures else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the locked APU-P7 KWS 1000-window corpus through the production RTL path."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
from apu_kws import ( # noqa: E402
|
||||||
|
APUM_BYTES,
|
||||||
|
APUM_PAYLOAD_CRC,
|
||||||
|
APUM_SHA256,
|
||||||
|
CORPUS_SHA256,
|
||||||
|
TFLITE_SHA256,
|
||||||
|
image_sha256,
|
||||||
|
parse_apum,
|
||||||
|
)
|
||||||
|
from apu_kws_convert import TfliteError, import_tflite # noqa: E402
|
||||||
|
from setup_apu_kws_reference import REQUIRED_ARCHIVE, REQUIRED_SOURCES # noqa: E402
|
||||||
|
|
||||||
|
MANIFEST = ROOT / "docs/ip/apu-kws-corpus.tsv"
|
||||||
|
PCM_SOURCES = ROOT / ".cache/retrosoc/sources/apu-kws-pcm"
|
||||||
|
KWS01 = ROOT / ".cache/retrosoc/sources/apu-kws-mfcc/datasets/kws01"
|
||||||
|
Y_LABELS = KWS01 / "y_labels.csv"
|
||||||
|
Y_LABELS_SHA256 = "6cb3709762d53fe64eb00eaf4a75796248bb3c1386f1d1f93ccdb07b071e9698"
|
||||||
|
AGGREGATE_PCM_SHA256 = "abd1ff2c988ac168dd68c29f934d95114d1094d7818d79fe73adb166ac009bee"
|
||||||
|
TFLITE = (
|
||||||
|
ROOT
|
||||||
|
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
|
||||||
|
/ "trained_models/kws_ref_model.tflite"
|
||||||
|
)
|
||||||
|
FIXTURE = ROOT / "tests/rtl/apu_p7_accuracy_tb.sv"
|
||||||
|
ROM = ROOT / "rtl/ip/multimedia/apu_kws_rom.svh"
|
||||||
|
WINDOW_COUNT = 1000
|
||||||
|
WINDOW_BYTES = 32000
|
||||||
|
WINDOW_SAMPLES = 16000
|
||||||
|
MINIMUM_TOP1 = 900
|
||||||
|
CLASS_COUNT = 12
|
||||||
|
SETUP_HINT = "install the locked P7 inputs with `make setup-apu-kws-reference`"
|
||||||
|
RESULT_PATTERN = re.compile(r"^KWS_RESULT (\d+) (\d+) (\d+) (\d+)$", re.MULTILINE)
|
||||||
|
COMPLETE_PATTERN = re.compile(r"^APU_P7_ACCURACY_COMPLETE windows=(\d+) first=(\d+)$", re.MULTILINE)
|
||||||
|
VARIANT_PATTERN = re.compile(r"^(?P<profile>.+)-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-[0-9a-f]{6,}$")
|
||||||
|
|
||||||
|
|
||||||
|
class QualificationError(RuntimeError):
|
||||||
|
"""A locked input, tool, or simulation step failed closed."""
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_bytes(data: bytes) -> str:
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as source:
|
||||||
|
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_file(path: Path, description: str) -> Path:
|
||||||
|
if not path.is_file():
|
||||||
|
raise QualificationError(f"missing {description}: {path}; {SETUP_HINT}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _git_identity(paths: list[Path] | None = None) -> dict[str, Any]:
|
||||||
|
command = ["git", "-C", str(ROOT)]
|
||||||
|
revision = subprocess.run(
|
||||||
|
[*command, "rev-parse", "HEAD"], text=True, capture_output=True, check=False
|
||||||
|
)
|
||||||
|
status_command = [*command, "status", "--porcelain"]
|
||||||
|
if paths is not None:
|
||||||
|
status_command.extend(("--", *[str(path) for path in paths]))
|
||||||
|
status = subprocess.run(status_command, text=True, capture_output=True, check=False)
|
||||||
|
return {
|
||||||
|
"git_revision": revision.stdout.strip() if revision.returncode == 0 else None,
|
||||||
|
"git_dirty": (bool(status.stdout.strip()) if status.returncode == 0 else None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_version(command: list[str]) -> str | None:
|
||||||
|
result = subprocess.run(command, text=True, capture_output=True, check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
line = (result.stdout or result.stderr).splitlines()
|
||||||
|
return line[0].strip() if line else None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_manifest() -> list[dict[str, Any]]:
|
||||||
|
_require_file(MANIFEST, "locked PCM manifest")
|
||||||
|
raw = MANIFEST.read_bytes()
|
||||||
|
text = raw.replace(b"\r\n", b"\n") if b"\r\n" in raw else raw
|
||||||
|
if _sha256_bytes(text) != CORPUS_SHA256:
|
||||||
|
raise QualificationError(
|
||||||
|
f"stale locked PCM manifest: {MANIFEST} canonical-text SHA-256 mismatch; {SETUP_HINT}"
|
||||||
|
)
|
||||||
|
lines = text.decode("ascii").split("\n")
|
||||||
|
if lines[-1] != "":
|
||||||
|
raise QualificationError("locked PCM manifest is missing its trailing LF")
|
||||||
|
rows = lines[:-1]
|
||||||
|
if len(rows) != WINDOW_COUNT:
|
||||||
|
raise QualificationError(f"locked PCM manifest has {len(rows)} rows, expected 1000")
|
||||||
|
records: list[dict[str, Any]] = []
|
||||||
|
for index, row in enumerate(rows):
|
||||||
|
fields = row.split("\t")
|
||||||
|
if len(fields) != 6:
|
||||||
|
raise QualificationError(f"locked PCM manifest row {index} does not have 6 columns")
|
||||||
|
bin_name, relpath, class_text, wav_sha256, pcm_sha256 = fields[1:]
|
||||||
|
if fields[0] != f"{index:06d}":
|
||||||
|
raise QualificationError(f"locked PCM manifest row {index} is out of order")
|
||||||
|
if relpath.startswith("/") or ".." in Path(relpath).parts:
|
||||||
|
raise QualificationError(f"locked PCM manifest row {index} has an unsafe path")
|
||||||
|
class_id = int(class_text)
|
||||||
|
if not 0 <= class_id < CLASS_COUNT:
|
||||||
|
raise QualificationError(f"locked PCM manifest row {index} class out of range")
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"index": index,
|
||||||
|
"bin_name": bin_name,
|
||||||
|
"relpath": relpath,
|
||||||
|
"class_id": class_id,
|
||||||
|
"wav_sha256": wav_sha256,
|
||||||
|
"pcm_sha256": pcm_sha256,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def _load_labels(records: list[dict[str, Any]]) -> list[int]:
|
||||||
|
_require_file(Y_LABELS, "official y_labels.csv")
|
||||||
|
if _sha256_file(Y_LABELS) != Y_LABELS_SHA256:
|
||||||
|
raise QualificationError(f"stale official y_labels.csv: {Y_LABELS}; {SETUP_HINT}")
|
||||||
|
lines = Y_LABELS.read_text(encoding="ascii").splitlines()
|
||||||
|
if len(lines) != WINDOW_COUNT:
|
||||||
|
raise QualificationError(f"official y_labels.csv has {len(lines)} rows, expected 1000")
|
||||||
|
labels: list[int] = []
|
||||||
|
for record, line in zip(records, lines):
|
||||||
|
name, total, label = line.split(",")
|
||||||
|
if name != record["bin_name"] or int(total) != CLASS_COUNT:
|
||||||
|
raise QualificationError(
|
||||||
|
f"official y_labels.csv row {record['index']} does not match the PCM manifest"
|
||||||
|
)
|
||||||
|
if int(label) != record["class_id"]:
|
||||||
|
raise QualificationError(
|
||||||
|
f"official y_labels.csv row {record['index']} label disagrees with the PCM manifest"
|
||||||
|
)
|
||||||
|
labels.append(int(label))
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def _padded_pcm(record: dict[str, Any]) -> bytes:
|
||||||
|
source = _require_file(PCM_SOURCES / record["relpath"], "locked source WAV")
|
||||||
|
wav = source.read_bytes()
|
||||||
|
if _sha256_bytes(wav) != record["wav_sha256"]:
|
||||||
|
raise QualificationError(f"source WAV SHA-256 mismatch: {source}; {SETUP_HINT}")
|
||||||
|
if len(wav) < 12 or wav[0:4] != b"RIFF" or wav[8:12] != b"WAVE":
|
||||||
|
raise QualificationError(f"source WAV is not RIFF/WAVE: {source}")
|
||||||
|
fmt: bytes | None = None
|
||||||
|
data: bytes | None = None
|
||||||
|
offset = 12
|
||||||
|
while offset + 8 <= len(wav):
|
||||||
|
chunk_size = struct.unpack_from("<I", wav, offset + 4)[0]
|
||||||
|
body = wav[offset + 8 : offset + 8 + chunk_size]
|
||||||
|
if len(body) != chunk_size:
|
||||||
|
raise QualificationError(f"source WAV chunk is truncated: {source}")
|
||||||
|
if wav[offset : offset + 4] == b"fmt ":
|
||||||
|
fmt = body
|
||||||
|
elif wav[offset : offset + 4] == b"data":
|
||||||
|
data = body
|
||||||
|
offset += 8 + chunk_size + (chunk_size & 1)
|
||||||
|
if fmt is None or len(fmt) < 16 or data is None:
|
||||||
|
raise QualificationError(f"source WAV lacks fmt/data chunks: {source}")
|
||||||
|
audio_format, channels, rate, _byte_rate, _align, bits = struct.unpack_from("<HHIIHH", fmt)
|
||||||
|
if (audio_format, channels, rate, bits) != (1, 1, WINDOW_SAMPLES, 16):
|
||||||
|
raise QualificationError(f"source WAV is not mono/16000/S16 PCM: {source}")
|
||||||
|
if len(data) > WINDOW_BYTES or len(data) % 2 != 0:
|
||||||
|
raise QualificationError(f"source WAV exceeds one 16000-sample window: {source}")
|
||||||
|
padded = data + bytes(WINDOW_BYTES - len(data))
|
||||||
|
if _sha256_bytes(padded) != record["pcm_sha256"]:
|
||||||
|
raise QualificationError(f"padded PCM SHA-256 mismatch for row {record['index']}: {source}")
|
||||||
|
return padded
|
||||||
|
|
||||||
|
|
||||||
|
def _write_hex(path: Path, payload: bytes) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(
|
||||||
|
"".join(
|
||||||
|
f"{int.from_bytes(payload[offset : offset + 4], 'little'):08x}\n"
|
||||||
|
for offset in range(0, len(payload), 4)
|
||||||
|
),
|
||||||
|
encoding="ascii",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _production_sources() -> list[Path]:
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
return [
|
||||||
|
common / "interface/axi4_stream_if.sv",
|
||||||
|
multimedia / "apu_kws_sram_client.sv",
|
||||||
|
multimedia / "apu_kws_engine.sv",
|
||||||
|
FIXTURE,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _rtl_inputs() -> list[Path]:
|
||||||
|
return [
|
||||||
|
*_production_sources(),
|
||||||
|
ROOT / "rtl/ip/multimedia/apu_define.svh",
|
||||||
|
ROM,
|
||||||
|
Path(__file__).resolve(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _verification_sha256() -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for path in sorted(_rtl_inputs()):
|
||||||
|
digest.update(path.relative_to(ROOT).as_posix().encode("utf-8"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(path.read_bytes())
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _subprocess_text(value: str | bytes | None) -> str:
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return value.decode("utf-8", errors="replace")
|
||||||
|
return value or ""
|
||||||
|
|
||||||
|
|
||||||
|
def _run_command(command: list[str], *, timeout: int | None = None) -> dict[str, Any]:
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
process = subprocess.run(command, check=False, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return {
|
||||||
|
"command": command,
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"stdout": process.stdout,
|
||||||
|
"stderr": process.stderr,
|
||||||
|
"timed_out": False,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired as error:
|
||||||
|
return {
|
||||||
|
"command": command,
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"exit_code": 124,
|
||||||
|
"stdout": _subprocess_text(error.stdout),
|
||||||
|
"stderr": _subprocess_text(error.stderr),
|
||||||
|
"timed_out": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compile(build_dir: Path, simulator: str) -> dict[str, Any]:
|
||||||
|
tools = {name: shutil.which(name) for name in ("sv2v", "iverilog", "vvp")}
|
||||||
|
missing = [name for name, path in tools.items() if path is None]
|
||||||
|
if missing:
|
||||||
|
raise QualificationError(f"missing required simulators: {', '.join(missing)}")
|
||||||
|
tools["verilator"] = shutil.which("verilator")
|
||||||
|
if simulator == "verilator" and tools["verilator"] is None:
|
||||||
|
raise QualificationError("--simulator verilator requires verilator on PATH")
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
filelist = build_dir / "apu_p7_accuracy.fl"
|
||||||
|
filelist.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"+define+SV_ASSRT_DISABLE",
|
||||||
|
f"+incdir+{common}",
|
||||||
|
f"+incdir+{common / 'interface'}",
|
||||||
|
f"+incdir+{multimedia}",
|
||||||
|
*(str(source) for source in _production_sources()),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
converted = build_dir / "apu_p7_accuracy.v"
|
||||||
|
conversion = _run_command(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||||
|
"-f",
|
||||||
|
str(filelist),
|
||||||
|
"--output",
|
||||||
|
str(converted),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if conversion["exit_code"] != 0:
|
||||||
|
raise QualificationError(f"APU-P7 accuracy sv2v failed: {conversion['stderr']}")
|
||||||
|
simv = build_dir / "apu_p7_accuracy_simv"
|
||||||
|
compile_result = _run_command(
|
||||||
|
[tools["iverilog"], "-g2012", "-s", "apu_p7_accuracy_tb", "-o", str(simv), str(converted)]
|
||||||
|
)
|
||||||
|
if compile_result["exit_code"] != 0:
|
||||||
|
raise QualificationError(f"APU-P7 accuracy Icarus compile failed: {compile_result['stderr']}")
|
||||||
|
verilator_binary: Path | None = None
|
||||||
|
verilator_log: dict[str, Any] | None = None
|
||||||
|
if tools["verilator"] is not None:
|
||||||
|
verilator_dir = build_dir / "verilator"
|
||||||
|
ccache_dir = build_dir / "ccache"
|
||||||
|
ccache_dir.mkdir(exist_ok=True)
|
||||||
|
environment = {
|
||||||
|
**os.environ,
|
||||||
|
"CCACHE_DIR": str(ccache_dir),
|
||||||
|
"CCACHE_TEMPDIR": str(ccache_dir),
|
||||||
|
}
|
||||||
|
started = time.monotonic()
|
||||||
|
process = subprocess.run(
|
||||||
|
[
|
||||||
|
str(tools["verilator"]),
|
||||||
|
"--binary",
|
||||||
|
"--timing",
|
||||||
|
"-O3",
|
||||||
|
"-Wno-fatal",
|
||||||
|
"--top-module",
|
||||||
|
"apu_p7_accuracy_tb",
|
||||||
|
"--Mdir",
|
||||||
|
str(verilator_dir),
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
verilator_log = {
|
||||||
|
"command": list(process.args),
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"stdout": process.stdout,
|
||||||
|
"stderr": process.stderr,
|
||||||
|
}
|
||||||
|
if process.returncode != 0:
|
||||||
|
if simulator == "verilator":
|
||||||
|
raise QualificationError(
|
||||||
|
f"APU-P7 accuracy Verilator compile failed: {process.stderr}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
verilator_binary = verilator_dir / "Vapu_p7_accuracy_tb"
|
||||||
|
if simulator == "verilator" and verilator_binary is None:
|
||||||
|
raise QualificationError("Verilator compile did not produce a simulation binary")
|
||||||
|
return {
|
||||||
|
"tools": tools,
|
||||||
|
"filelist": filelist,
|
||||||
|
"converted": converted,
|
||||||
|
"simv": simv,
|
||||||
|
"verilator": verilator_binary,
|
||||||
|
"sv2v_log": conversion,
|
||||||
|
"iverilog_log": compile_result,
|
||||||
|
"verilator_log": verilator_log,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_name(build_dir: Path, override: str | None) -> str:
|
||||||
|
if override:
|
||||||
|
return override
|
||||||
|
parts = build_dir.resolve().parts
|
||||||
|
if len(parts) >= 4 and parts[-2:] == ("apu", "kws") and parts[-4] == "build":
|
||||||
|
match = VARIANT_PATTERN.match(parts[-3])
|
||||||
|
if match:
|
||||||
|
return match.group("profile")
|
||||||
|
return "standalone"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_predictions(stdout: str, window_count: int) -> list[dict[str, int]]:
|
||||||
|
predictions: dict[int, dict[str, int]] = {}
|
||||||
|
for match in RESULT_PATTERN.finditer(stdout):
|
||||||
|
index, class_id, score, hit = (int(value) for value in match.groups())
|
||||||
|
if index in predictions:
|
||||||
|
raise QualificationError(f"duplicated utterance index {index} in simulator output")
|
||||||
|
if class_id >= CLASS_COUNT:
|
||||||
|
raise QualificationError(f"simulator emitted invalid class {class_id}")
|
||||||
|
predictions[index] = {"index": index, "predicted": class_id, "score": score, "hit": hit}
|
||||||
|
complete = COMPLETE_PATTERN.search(stdout)
|
||||||
|
if complete is None or int(complete.group(1)) != window_count or int(complete.group(2)) != 0:
|
||||||
|
raise QualificationError("simulator did not emit the expected completion marker")
|
||||||
|
expected = set(range(window_count))
|
||||||
|
observed = set(predictions)
|
||||||
|
if observed != expected:
|
||||||
|
raise QualificationError(
|
||||||
|
f"skipped utterances: missing {sorted(expected - observed)[:8]} "
|
||||||
|
f"unexpected {sorted(observed - expected)[:8]}"
|
||||||
|
)
|
||||||
|
return [predictions[index] for index in sorted(predictions)]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--build-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--max-windows", type=int, default=0,
|
||||||
|
help="run only the first N locked windows (0 = all 1000)")
|
||||||
|
parser.add_argument("--report", type=str, default="accuracy.json")
|
||||||
|
parser.add_argument("--profile", type=str, default=None)
|
||||||
|
parser.add_argument("--timeout-seconds", type=int, default=21600)
|
||||||
|
parser.add_argument("--max-window-cycles", type=int, default=4000000)
|
||||||
|
parser.add_argument(
|
||||||
|
"--simulator",
|
||||||
|
choices=("auto", "iverilog", "verilator"),
|
||||||
|
default="auto",
|
||||||
|
help="run simulator; auto prefers Verilator (Icarus is ~400x slower per window)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not 0 <= args.max_windows <= WINDOW_COUNT:
|
||||||
|
raise SystemExit("--max-windows must be within 0..1000")
|
||||||
|
window_count = WINDOW_COUNT if args.max_windows == 0 else args.max_windows
|
||||||
|
build_dir = args.build_dir.resolve()
|
||||||
|
started_utc = datetime.now(timezone.utc).isoformat()
|
||||||
|
started = time.monotonic()
|
||||||
|
report_path = build_dir / args.report
|
||||||
|
|
||||||
|
try:
|
||||||
|
records = _load_manifest()
|
||||||
|
labels = _load_labels(records)
|
||||||
|
image = import_tflite(_require_file(TFLITE, "locked kws_ref_model.tflite"))
|
||||||
|
parse_apum(image)
|
||||||
|
pcm_dir = build_dir / "pcm"
|
||||||
|
aggregate = hashlib.sha256()
|
||||||
|
for record in records[:window_count]:
|
||||||
|
padded = _padded_pcm(record)
|
||||||
|
aggregate.update(padded)
|
||||||
|
_write_hex(pcm_dir / f"pcm_{record['index']:06d}.hex", padded)
|
||||||
|
aggregate_sha256 = aggregate.hexdigest()
|
||||||
|
if window_count == WINDOW_COUNT and aggregate_sha256 != AGGREGATE_PCM_SHA256:
|
||||||
|
raise QualificationError(
|
||||||
|
"aggregate padded PCM SHA-256 does not match the frozen 1000-window value"
|
||||||
|
)
|
||||||
|
apum_hex = build_dir / "kws_apum.hex"
|
||||||
|
_write_hex(apum_hex, image)
|
||||||
|
built = _compile(build_dir, args.simulator)
|
||||||
|
except (OSError, TfliteError, ValueError, QualificationError) as error:
|
||||||
|
raise SystemExit(str(error)) from error
|
||||||
|
|
||||||
|
selected = (
|
||||||
|
"verilator"
|
||||||
|
if (args.simulator == "verilator" or (args.simulator == "auto" and built["verilator"]))
|
||||||
|
else "icarus"
|
||||||
|
)
|
||||||
|
plusargs = [
|
||||||
|
f"+APUM_HEX={apum_hex}",
|
||||||
|
f"+PCM_DIR={pcm_dir}",
|
||||||
|
f"+WINDOW_COUNT={window_count}",
|
||||||
|
"+FIRST_INDEX=0",
|
||||||
|
f"+MAX_WINDOW_CYCLES={args.max_window_cycles}",
|
||||||
|
]
|
||||||
|
command = (
|
||||||
|
[str(built["verilator"]), *plusargs]
|
||||||
|
if selected == "verilator"
|
||||||
|
else [str(built["tools"]["vvp"]), str(built["simv"]), *plusargs]
|
||||||
|
)
|
||||||
|
execution = _run_command(command, timeout=args.timeout_seconds)
|
||||||
|
log_path = build_dir / "sim.log"
|
||||||
|
log_path.write_text(execution["stdout"] + execution["stderr"], encoding="utf-8")
|
||||||
|
|
||||||
|
failure: str | None = None
|
||||||
|
predictions: list[dict[str, int]] = []
|
||||||
|
if execution["timed_out"]:
|
||||||
|
failure = f"simulator timeout after {args.timeout_seconds} seconds"
|
||||||
|
elif execution["exit_code"] != 0:
|
||||||
|
failure = f"simulator failed with exit code {execution['exit_code']}"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
predictions = _parse_predictions(execution["stdout"], window_count)
|
||||||
|
except QualificationError as error:
|
||||||
|
failure = str(error)
|
||||||
|
|
||||||
|
for prediction in predictions:
|
||||||
|
prediction["expected"] = labels[prediction["index"]]
|
||||||
|
correct = sum(1 for item in predictions if item["predicted"] == item["expected"])
|
||||||
|
incorrect = [item["index"] for item in predictions if item["predicted"] != item["expected"]]
|
||||||
|
per_class = [
|
||||||
|
{"class_id": class_id, "total": 0, "correct": 0, "incorrect": 0}
|
||||||
|
for class_id in range(CLASS_COUNT)
|
||||||
|
]
|
||||||
|
confusion = [[0] * CLASS_COUNT for _ in range(CLASS_COUNT)]
|
||||||
|
for item in predictions:
|
||||||
|
expected, predicted = item["expected"], item["predicted"]
|
||||||
|
confusion[expected][predicted] += 1
|
||||||
|
per_class[expected]["total"] += 1
|
||||||
|
if expected == predicted:
|
||||||
|
per_class[expected]["correct"] += 1
|
||||||
|
else:
|
||||||
|
per_class[expected]["incorrect"] += 1
|
||||||
|
gate_applies = window_count == WINDOW_COUNT
|
||||||
|
gate_passed = correct >= MINIMUM_TOP1
|
||||||
|
success = failure is None and (not gate_applies or gate_passed)
|
||||||
|
if failure is None and gate_applies and not gate_passed:
|
||||||
|
failure = f"top-1 accuracy {correct}/1000 is below the required 900/1000"
|
||||||
|
|
||||||
|
rtl_inputs = _rtl_inputs()
|
||||||
|
report: dict[str, Any] = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"report": "accuracy",
|
||||||
|
"target": "p7",
|
||||||
|
"profile": _profile_name(build_dir, args.profile),
|
||||||
|
"success": success,
|
||||||
|
"failure": failure,
|
||||||
|
"gate": {
|
||||||
|
"window_count": WINDOW_COUNT,
|
||||||
|
"minimum_top1": MINIMUM_TOP1,
|
||||||
|
"applies": gate_applies,
|
||||||
|
"passed": gate_passed if gate_applies else None,
|
||||||
|
},
|
||||||
|
"seed": 0,
|
||||||
|
"command": [sys.executable, str(Path(__file__).resolve()), *sys.argv[1:]],
|
||||||
|
"started_utc": started_utc,
|
||||||
|
"duration_seconds": round(time.monotonic() - started, 3),
|
||||||
|
"counts": {
|
||||||
|
"windows_requested": window_count,
|
||||||
|
"windows_completed": len(predictions),
|
||||||
|
"top1_correct": correct,
|
||||||
|
"top1_accuracy": round(correct / len(predictions), 6) if predictions else 0.0,
|
||||||
|
"hits": sum(item["hit"] for item in predictions),
|
||||||
|
"incorrect": len(incorrect),
|
||||||
|
},
|
||||||
|
"per_class": per_class,
|
||||||
|
"confusion_matrix": confusion,
|
||||||
|
"incorrect_indices": incorrect,
|
||||||
|
"predictions": predictions,
|
||||||
|
"inputs": {
|
||||||
|
"manifest": {
|
||||||
|
"path": str(MANIFEST.relative_to(ROOT)),
|
||||||
|
"sha256": CORPUS_SHA256,
|
||||||
|
},
|
||||||
|
"y_labels": {"path": str(Y_LABELS.relative_to(ROOT)), "sha256": Y_LABELS_SHA256},
|
||||||
|
"tflite": {"path": str(TFLITE.relative_to(ROOT)), "sha256": TFLITE_SHA256},
|
||||||
|
"pcm_sources": {
|
||||||
|
"path": str(PCM_SOURCES.relative_to(ROOT)),
|
||||||
|
"windows": window_count,
|
||||||
|
"source_sha256_verified": True,
|
||||||
|
"padded_pcm_sha256_verified": True,
|
||||||
|
"aggregate_padded_pcm_sha256": aggregate_sha256,
|
||||||
|
},
|
||||||
|
"reference_sources": REQUIRED_SOURCES,
|
||||||
|
"reference_archives": REQUIRED_ARCHIVE,
|
||||||
|
},
|
||||||
|
"converter": {
|
||||||
|
"path": "scripts/apu_kws_convert.py",
|
||||||
|
"sha256": _sha256_file(ROOT / "scripts/apu_kws_convert.py"),
|
||||||
|
"contract_revision": "apu-kws-convert/1.0.0",
|
||||||
|
**_git_identity(),
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"path": "scripts/apu_kws.py",
|
||||||
|
"sha256": _sha256_file(ROOT / "scripts/apu_kws.py"),
|
||||||
|
},
|
||||||
|
"rtl": {
|
||||||
|
**_git_identity(rtl_inputs),
|
||||||
|
"fixture": str(FIXTURE.relative_to(ROOT)),
|
||||||
|
"verification_sha256": _verification_sha256(),
|
||||||
|
"sources": [
|
||||||
|
{"path": str(path.relative_to(ROOT)), "sha256": _sha256_file(path)}
|
||||||
|
for path in rtl_inputs
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"apum_sha256": image_sha256(image),
|
||||||
|
"payload_crc": f"0x{APUM_PAYLOAD_CRC:08x}",
|
||||||
|
"bytes": APUM_BYTES,
|
||||||
|
"frozen_sha256": APUM_SHA256,
|
||||||
|
},
|
||||||
|
"rom": {"path": str(ROM.relative_to(ROOT)), "sha256": _sha256_file(ROM)},
|
||||||
|
"tools": {
|
||||||
|
"python": sys.version.split()[0],
|
||||||
|
"sv2v": _tool_version([str(built["tools"]["sv2v"]), "--version"]),
|
||||||
|
"iverilog": _tool_version([str(built["tools"]["iverilog"]), "-V"]),
|
||||||
|
"vvp": _tool_version([str(built["tools"]["vvp"]), "-V"]),
|
||||||
|
"verilator": (
|
||||||
|
_tool_version([str(built["tools"]["verilator"]), "--version"])
|
||||||
|
if built["tools"]["verilator"] is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"simulation": {
|
||||||
|
"simulator": selected,
|
||||||
|
"command": command,
|
||||||
|
"exit_code": execution["exit_code"],
|
||||||
|
"timed_out": execution["timed_out"],
|
||||||
|
"duration_seconds": execution["duration_seconds"],
|
||||||
|
"log": str(log_path),
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"build_dir": str(build_dir),
|
||||||
|
"report": str(report_path),
|
||||||
|
"apum_hex": str(apum_hex),
|
||||||
|
"pcm_dir": str(pcm_dir),
|
||||||
|
"filelist": str(built["filelist"]),
|
||||||
|
"converted": str(built["converted"]),
|
||||||
|
"simv": str(built["simv"]),
|
||||||
|
"verilator": str(built["verilator"]) if built["verilator"] is not None else None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_write_json(report_path, report)
|
||||||
|
if gate_applies:
|
||||||
|
verdict = f"gate 900/1000 {'PASS' if gate_passed else 'FAIL'}"
|
||||||
|
else:
|
||||||
|
verdict = "subset, gate not applied"
|
||||||
|
print(f"APU-P7 KWS accuracy: top-1 {correct}/{len(predictions)} ({verdict})")
|
||||||
|
print(f"report: {report_path}")
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -30,6 +30,9 @@ tooling.
|
|||||||
every pinned official FLAC file against locked libFLAC and records matching
|
every pinned official FLAC file against locked libFLAC and records matching
|
||||||
released-APUMC production results from identical verification-only Icarus
|
released-APUMC production results from identical verification-only Icarus
|
||||||
and Verilator fixtures.
|
and Verilator fixtures.
|
||||||
|
- `test_apu_p8_quiesce_load.py` drives the full `apb4_apu` shell and checks
|
||||||
|
that MICROCODE_LOAD and MODEL_LOAD DMA fetches are admitted and complete
|
||||||
|
while the LP quiesce is held (the APU-P8 loader-admission regression).
|
||||||
- `test_user_ip_register_parity.py` keeps the integrated slot 1 timer and slot
|
- `test_user_ip_register_parity.py` keeps the integrated slot 1 timer and slot
|
||||||
2 GPIO register offsets synchronized with their application-owned C
|
2 GPIO register offsets synchronized with their application-owned C
|
||||||
definitions and checks their extension-manifest slot assignments.
|
definitions and checks their extension-manifest slot assignments.
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
// SPDX-License-Identifier: MulanPSL-2.0
|
||||||
|
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// APU-P7 KWS end-to-end accuracy qualification fixture. Each window is a
|
||||||
|
// plusarg-selected 32000-byte padded PCM image driven through operation 1
|
||||||
|
// (one KWS memory window) on the real frontend/inference RTL path with the
|
||||||
|
// frozen APUM model resident in the KWS SRAM model reservation. Classifier
|
||||||
|
// history and counters are reset at every idle boundary between windows.
|
||||||
|
// The fixture fails closed on any DMA protocol, error, or timeout condition.
|
||||||
|
module apu_p7_accuracy_tb;
|
||||||
|
localparam int unsigned PcmWords = 8000;
|
||||||
|
localparam int unsigned ModelWords = 8192;
|
||||||
|
localparam logic [15:0] KwsConfig = 16'h0180; // debounce=1, threshold=128
|
||||||
|
localparam logic [31:0] InputConfig = 32'h0102_3e80; // mono/16000/16 S16_LE
|
||||||
|
localparam logic [31:0] MemoryBase = 32'h0001_0000;
|
||||||
|
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic clear_history = 1'b0;
|
||||||
|
logic counter_clear = 1'b0;
|
||||||
|
logic memory_start = 1'b0;
|
||||||
|
logic memory_dma_request_ready = 1'b0;
|
||||||
|
logic [31:0] memory_dma_data = 32'd0;
|
||||||
|
logic [ 3:0] memory_dma_keep = 4'd0;
|
||||||
|
logic memory_dma_last = 1'b0;
|
||||||
|
logic memory_dma_valid = 1'b0;
|
||||||
|
logic memory_dma_done = 1'b0;
|
||||||
|
logic [31:0] status_o, result_o, frame_count_o, inference_count_o, hit_count_o;
|
||||||
|
logic [31:0] overrun_count_o, timestamp_lo_o, timestamp_hi_o;
|
||||||
|
logic [31:0] model_status_o, model_actual_crc_o, memory_input_used_o;
|
||||||
|
logic memory_start_ready_o;
|
||||||
|
logic memory_dma_request_valid_o, memory_done_o, memory_error_o;
|
||||||
|
logic [31:0] memory_dma_request_address_o, memory_dma_request_bytes_o;
|
||||||
|
logic [5:0] memory_error_code_o;
|
||||||
|
logic [3:0] memory_error_stage_o;
|
||||||
|
logic [1:0] memory_error_resp_o;
|
||||||
|
logic [31:0] memory_error_address_o, memory_error_detail_o;
|
||||||
|
logic [10:0] irq_set_o;
|
||||||
|
logic fault_valid_o, model_valid_o, model_lock_o, busy_o, idle_o, rx_ready_o;
|
||||||
|
logic [5:0] fault_code_o;
|
||||||
|
logic [3:0] fault_stage_o;
|
||||||
|
logic [1:0] fault_resp_o;
|
||||||
|
logic [7:0] fault_index_o;
|
||||||
|
logic [31:0] fault_addr_o, fault_detail_o;
|
||||||
|
logic [15:0][14:0] model_addr_o;
|
||||||
|
logic [15:0][ 7:0] model_store_data;
|
||||||
|
logic scratch_clear;
|
||||||
|
logic [19:0][15:0] scratch_read_addr;
|
||||||
|
logic [19:0][31:0] scratch_read_data;
|
||||||
|
logic [ 5:0] scratch_write_valid;
|
||||||
|
logic [ 5:0][15:0] scratch_write_addr;
|
||||||
|
logic [ 5:0][31:0] scratch_write_data;
|
||||||
|
logic [ 5:0][ 3:0] scratch_write_strb;
|
||||||
|
logic scratch_access_err;
|
||||||
|
logic [31:0] pcm_mem [0:PcmWords-1];
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) stream_i (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
assign stream_i.tvalid = 1'b0;
|
||||||
|
assign stream_i.tdata = 32'd0;
|
||||||
|
assign stream_i.tkeep = 4'hf;
|
||||||
|
assign stream_i.tstrb = 4'hf;
|
||||||
|
assign stream_i.tlast = 1'b0;
|
||||||
|
assign stream_i.tid = '0;
|
||||||
|
assign stream_i.tdest = '0;
|
||||||
|
assign stream_i.tuser = '0;
|
||||||
|
assign stream_i.tready = rx_ready_o;
|
||||||
|
|
||||||
|
apu_kws_sram_client u_kws_sram_client (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i (rst_n_i),
|
||||||
|
.req_i (1'b0),
|
||||||
|
.write_i (1'b0),
|
||||||
|
.addr_i (17'd0),
|
||||||
|
.data_i (32'd0),
|
||||||
|
.strb_i (4'd0),
|
||||||
|
.ready_o (),
|
||||||
|
.data_o (),
|
||||||
|
.valid_o (),
|
||||||
|
.access_err_o (),
|
||||||
|
.model_addr_i (model_addr_o),
|
||||||
|
.model_data_o (model_store_data),
|
||||||
|
.scratch_clear_i (scratch_clear),
|
||||||
|
.scratch_read_addr_i (scratch_read_addr),
|
||||||
|
.scratch_read_data_o (scratch_read_data),
|
||||||
|
.scratch_write_valid_i(scratch_write_valid),
|
||||||
|
.scratch_write_addr_i (scratch_write_addr),
|
||||||
|
.scratch_write_data_i (scratch_write_data),
|
||||||
|
.scratch_write_strb_i (scratch_write_strb),
|
||||||
|
.scratch_access_err_o (scratch_access_err)
|
||||||
|
);
|
||||||
|
|
||||||
|
apu_kws_engine dut (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i (rst_n_i),
|
||||||
|
.soft_reset_i (1'b0),
|
||||||
|
.resource_reset_i (1'b0),
|
||||||
|
.counter_clear_i (counter_clear),
|
||||||
|
.xrun_clear_i (1'b0),
|
||||||
|
.abort_i (1'b0),
|
||||||
|
.quiesce_i (1'b0),
|
||||||
|
.disable_i (1'b0),
|
||||||
|
.flush_busy_i (1'b0),
|
||||||
|
.enable_i (1'b1),
|
||||||
|
.memory_window_i (1'b1),
|
||||||
|
.clear_history_i (clear_history),
|
||||||
|
.model_valid_i (1'b1),
|
||||||
|
.model_lock_i (1'b1),
|
||||||
|
.sequencer_timeout_i (32'd0),
|
||||||
|
.memory_start_i (memory_start),
|
||||||
|
.memory_start_ready_o (memory_start_ready_o),
|
||||||
|
.memory_address_i (MemoryBase),
|
||||||
|
.memory_dma_request_valid_o (memory_dma_request_valid_o),
|
||||||
|
.memory_dma_request_ready_i (memory_dma_request_ready),
|
||||||
|
.memory_dma_request_address_o(memory_dma_request_address_o),
|
||||||
|
.memory_dma_request_bytes_o (memory_dma_request_bytes_o),
|
||||||
|
.memory_dma_data_i (memory_dma_data),
|
||||||
|
.memory_dma_keep_i (memory_dma_keep),
|
||||||
|
.memory_dma_last_i (memory_dma_last),
|
||||||
|
.memory_dma_valid_i (memory_dma_valid),
|
||||||
|
.memory_dma_ready_o (memory_dma_ready_o),
|
||||||
|
.memory_dma_done_i (memory_dma_done),
|
||||||
|
.memory_dma_error_i (1'b0),
|
||||||
|
.memory_dma_error_code_i (6'd0),
|
||||||
|
.memory_dma_error_stage_i (4'd0),
|
||||||
|
.memory_dma_error_resp_i (2'd0),
|
||||||
|
.memory_dma_error_address_i (32'd0),
|
||||||
|
.memory_done_o (memory_done_o),
|
||||||
|
.memory_error_o (memory_error_o),
|
||||||
|
.memory_error_code_o (memory_error_code_o),
|
||||||
|
.memory_error_stage_o (memory_error_stage_o),
|
||||||
|
.memory_error_resp_o (memory_error_resp_o),
|
||||||
|
.memory_error_address_o (memory_error_address_o),
|
||||||
|
.memory_error_detail_o (memory_error_detail_o),
|
||||||
|
.memory_input_used_o (memory_input_used_o),
|
||||||
|
.kws_config_i (KwsConfig),
|
||||||
|
.input_config_i (InputConfig),
|
||||||
|
.model_addr_o (model_addr_o),
|
||||||
|
.model_data_i (model_store_data),
|
||||||
|
.scratch_clear_o (scratch_clear),
|
||||||
|
.scratch_read_addr_o (scratch_read_addr),
|
||||||
|
.scratch_read_data_i (scratch_read_data),
|
||||||
|
.scratch_write_valid_o (scratch_write_valid),
|
||||||
|
.scratch_write_addr_o (scratch_write_addr),
|
||||||
|
.scratch_write_data_o (scratch_write_data),
|
||||||
|
.scratch_write_strb_o (scratch_write_strb),
|
||||||
|
.scratch_access_err_i (scratch_access_err),
|
||||||
|
.stream_i (stream_i),
|
||||||
|
.rx_ready_o (rx_ready_o),
|
||||||
|
.status_o (status_o),
|
||||||
|
.result_o (result_o),
|
||||||
|
.timestamp_lo_o (timestamp_lo_o),
|
||||||
|
.timestamp_hi_o (timestamp_hi_o),
|
||||||
|
.frame_count_o (frame_count_o),
|
||||||
|
.inference_count_o (inference_count_o),
|
||||||
|
.hit_count_o (hit_count_o),
|
||||||
|
.overrun_count_o (overrun_count_o),
|
||||||
|
.model_status_o (model_status_o),
|
||||||
|
.model_actual_crc_o (model_actual_crc_o),
|
||||||
|
.irq_set_o (irq_set_o),
|
||||||
|
.fault_valid_o (fault_valid_o),
|
||||||
|
.fault_code_o (fault_code_o),
|
||||||
|
.fault_stage_o (fault_stage_o),
|
||||||
|
.fault_resp_o (fault_resp_o),
|
||||||
|
.fault_index_o (fault_index_o),
|
||||||
|
.fault_addr_o (fault_addr_o),
|
||||||
|
.fault_detail_o (fault_detail_o),
|
||||||
|
.model_valid_o (model_valid_o),
|
||||||
|
.model_lock_o (model_lock_o),
|
||||||
|
.busy_o (busy_o),
|
||||||
|
.idle_o (idle_o)
|
||||||
|
);
|
||||||
|
|
||||||
|
task automatic run_window(input int unsigned window_index, input string pcm_dir,
|
||||||
|
input int unsigned max_window_cycles);
|
||||||
|
string pcm_path;
|
||||||
|
int unsigned word_index;
|
||||||
|
int unsigned elapsed;
|
||||||
|
begin
|
||||||
|
pcm_path = $sformatf("%s/pcm_%06d.hex", pcm_dir, window_index);
|
||||||
|
$readmemh(pcm_path, pcm_mem);
|
||||||
|
@(negedge clk_i);
|
||||||
|
clear_history = 1'b1;
|
||||||
|
counter_clear = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
clear_history = 1'b0;
|
||||||
|
counter_clear = 1'b0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
if (!memory_start_ready_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d was not accepted at the idle boundary", window_index);
|
||||||
|
end
|
||||||
|
memory_start = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_start = 1'b0;
|
||||||
|
elapsed = 0;
|
||||||
|
while (!memory_dma_request_valid_o) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
elapsed++;
|
||||||
|
if (elapsed > 1000)
|
||||||
|
$fatal(1, "KWS memory window %0d never raised its DMA request", window_index);
|
||||||
|
end
|
||||||
|
if ((memory_dma_request_address_o != MemoryBase) ||
|
||||||
|
(memory_dma_request_bytes_o != 32'd32000)) begin
|
||||||
|
$fatal(1, "KWS memory window %0d DMA request mismatch", window_index);
|
||||||
|
end
|
||||||
|
memory_dma_request_ready = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_dma_request_ready = 1'b0;
|
||||||
|
for (word_index = 0; word_index < PcmWords; word_index++) begin
|
||||||
|
memory_dma_data = pcm_mem[word_index];
|
||||||
|
memory_dma_keep = 4'hf;
|
||||||
|
memory_dma_last = word_index == PcmWords - 1;
|
||||||
|
memory_dma_valid = 1'b1;
|
||||||
|
do @(negedge clk_i); while (!memory_dma_ready_o);
|
||||||
|
end
|
||||||
|
memory_dma_valid = 1'b0;
|
||||||
|
memory_dma_last = 1'b0;
|
||||||
|
memory_dma_done = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_dma_done = 1'b0;
|
||||||
|
elapsed = 0;
|
||||||
|
while (!memory_done_o && (elapsed < max_window_cycles)) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
elapsed++;
|
||||||
|
end
|
||||||
|
if (!memory_done_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d did not complete within %0d cycles", window_index,
|
||||||
|
max_window_cycles);
|
||||||
|
end
|
||||||
|
if (memory_error_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d error code=%0d stage=%0d address=%08x detail=%08x",
|
||||||
|
window_index, memory_error_code_o, memory_error_stage_o, memory_error_address_o,
|
||||||
|
memory_error_detail_o);
|
||||||
|
end
|
||||||
|
if (memory_input_used_o != 32'd32000) begin
|
||||||
|
$fatal(1, "KWS memory window %0d consumed %0d input bytes", window_index,
|
||||||
|
memory_input_used_o);
|
||||||
|
end
|
||||||
|
if ((frame_count_o != 32'd49) || (inference_count_o != 32'd1) || !status_o[7] ||
|
||||||
|
fault_valid_o) begin
|
||||||
|
$fatal(1,
|
||||||
|
"KWS memory window %0d frames=%0d inferences=%0d status=%08x fault=%0b/%0d/%0d/%08x",
|
||||||
|
window_index, frame_count_o, inference_count_o, status_o, fault_valid_o,
|
||||||
|
fault_code_o, fault_stage_o, fault_detail_o);
|
||||||
|
end
|
||||||
|
$display("KWS_RESULT %0d %0d %0d %0d", window_index, result_o[7:0], result_o[15:8],
|
||||||
|
result_o[16]);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
string apum_hex;
|
||||||
|
string pcm_dir;
|
||||||
|
int unsigned first_index;
|
||||||
|
int unsigned window_count;
|
||||||
|
int unsigned max_window_cycles;
|
||||||
|
|
||||||
|
if (!$value$plusargs("APUM_HEX=%s", apum_hex)) $fatal(1, "missing +APUM_HEX=<path>");
|
||||||
|
if (!$value$plusargs("PCM_DIR=%s", pcm_dir)) $fatal(1, "missing +PCM_DIR=<path>");
|
||||||
|
if (!$value$plusargs("WINDOW_COUNT=%d", window_count)) begin
|
||||||
|
$fatal(1, "missing +WINDOW_COUNT=<n>");
|
||||||
|
end
|
||||||
|
if (!$value$plusargs("FIRST_INDEX=%d", first_index)) first_index = 0;
|
||||||
|
if (!$value$plusargs("MAX_WINDOW_CYCLES=%d", max_window_cycles)) begin
|
||||||
|
max_window_cycles = 4000000;
|
||||||
|
end
|
||||||
|
if (window_count == 0) $fatal(1, "WINDOW_COUNT must be positive");
|
||||||
|
|
||||||
|
repeat (4) @(negedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
if (!model_valid_o || !model_lock_o) $fatal(1, "KWS model admission was not observed");
|
||||||
|
$readmemh(apum_hex, u_kws_sram_client.mem, 0, ModelWords - 1);
|
||||||
|
|
||||||
|
for (int unsigned window = first_index; window < first_index + window_count; window++) begin
|
||||||
|
run_window(window, pcm_dir, max_window_cycles);
|
||||||
|
end
|
||||||
|
$display("APU_P7_ACCURACY_COMPLETE windows=%0d first=%0d", window_count, first_index);
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,771 @@
|
|||||||
|
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
// SPDX-License-Identifier: MulanPSL-2.0
|
||||||
|
|
||||||
|
// APU-P7 concurrent qualification: continuous RX KWS running together with
|
||||||
|
// looped WAV/FLAC decode streamed to I2S TX under the frozen P5 ready-memory
|
||||||
|
// discipline (64-word prefill, then ready memory without injected stalls).
|
||||||
|
// The bench fails closed on any codec underrun, I2S RX overrun, KWS overrun,
|
||||||
|
// missed KWS window schedule, or byte/frame/cycle/stall scoreboard mismatch.
|
||||||
|
|
||||||
|
`include "apu_define.svh"
|
||||||
|
|
||||||
|
module apu_p7_concurrent_tb;
|
||||||
|
localparam logic [31:0] ApuBase = 32'h1001_3000;
|
||||||
|
localparam logic [31:0] ImageBase = 32'h3000_0000;
|
||||||
|
localparam logic [31:0] ModelBase = 32'h3001_0000;
|
||||||
|
localparam logic [31:0] WavBase = 32'h3002_0000;
|
||||||
|
localparam logic [31:0] FlacBase = 32'h3003_0000;
|
||||||
|
localparam logic [31:0] WriteBase = 32'h3004_0000;
|
||||||
|
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic resource_reset_i;
|
||||||
|
logic [31:0] image [0:16383];
|
||||||
|
logic [31:0] model [ 0:8191];
|
||||||
|
logic [31:0] wav_image [ 0:8191];
|
||||||
|
logic [31:0] flac_image [ 0:1023];
|
||||||
|
logic [31:0] wav_pcm [0:16383];
|
||||||
|
logic [31:0] flac_pcm [0:16383];
|
||||||
|
logic read_active_q;
|
||||||
|
logic [31:0] read_addr_q;
|
||||||
|
logic [7:0] read_len_q, read_beat_q;
|
||||||
|
logic [63:0] cycle_q;
|
||||||
|
logic [31:0] s_value;
|
||||||
|
|
||||||
|
// Scenario plusarg state.
|
||||||
|
string apumc_path, apum_path, wav_path, flac_path, wav_pcm_path, flac_pcm_path;
|
||||||
|
logic [31:0] wav_bytes, wav_frames, wav_out_words;
|
||||||
|
logic [31:0] flac_bytes, flac_frames, flac_out_words;
|
||||||
|
logic [31:0] rate_hz, precision_bits, seconds_ms, seed_value, workload_mask, job_log;
|
||||||
|
logic [31:0] wav_source_rate;
|
||||||
|
logic [63:0] duration_cycles, max_cycles;
|
||||||
|
logic [31:0] output_config, wav_input_config, wav_control, kws_input_config;
|
||||||
|
logic [10:0] frame_period; // PCLK cycles between consumed words at the I2S cadence.
|
||||||
|
logic [ 2:0] decimation;
|
||||||
|
logic [ 1:0] rx_words_per_frame;
|
||||||
|
|
||||||
|
// I2S TX playback model state (64-word prefill, then periodic consumption).
|
||||||
|
// The 1024-word bench FIFO plus the frozen 64-word router FIFO model the
|
||||||
|
// product-line playback buffering; the frozen qualification fixes the
|
||||||
|
// 64-word prefill and ready memory, not the playback buffer depth.
|
||||||
|
logic [10:0] pb_count_q;
|
||||||
|
logic [10:0] pb_div_q;
|
||||||
|
logic pb_started_q, pb_done_q, pb_xrun_q;
|
||||||
|
logic [63:0] pb_first_cycle_q, pb_last_cycle_q, pb_consumed_q;
|
||||||
|
logic [31:0] pb_underrun_q;
|
||||||
|
logic drain_q;
|
||||||
|
|
||||||
|
// I2S RX real-time feed state (one word per frame tick, never re-presented).
|
||||||
|
logic rx_feed_q;
|
||||||
|
logic rx_valid_q;
|
||||||
|
logic [31:0] rx_data_q, rx_lfsr_q;
|
||||||
|
logic [63:0] rx_delivered_q, rx_presented_q;
|
||||||
|
logic [63:0] run_start_q, feed_stop_cycle_q;
|
||||||
|
logic [31:0] rx_overrun_q;
|
||||||
|
logic rx_overrun_sticky_q;
|
||||||
|
|
||||||
|
// Streamed-output scoreboard state.
|
||||||
|
logic run_active_q, perf_count_q;
|
||||||
|
logic pat_is_wav, pat_arm_q, pat_arm_seen_q;
|
||||||
|
logic [14:0] pat_offset_q;
|
||||||
|
logic [63:0] tx_words_q, tx_tlast_q;
|
||||||
|
logic [63:0] tx_stall_cycles_q, rx_stall_cycles_q;
|
||||||
|
logic [63:0] dma_read_stall_cycles_q, dma_write_stall_cycles_q;
|
||||||
|
|
||||||
|
apb4_if apb4 (
|
||||||
|
.pclk (clk_i),
|
||||||
|
.presetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) axi4 (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) dma_tx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) dma_rx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) i2s_tx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) i2s_rx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
// Ready-memory AXI read model: no injected stalls, back-to-back beats.
|
||||||
|
assign axi4.arready = !read_active_q;
|
||||||
|
assign axi4.rid = 1'b0;
|
||||||
|
assign axi4.rdata = (read_addr_q >= FlacBase) ? flac_image[(read_addr_q-FlacBase)>>2] :
|
||||||
|
((read_addr_q >= WavBase) ? wav_image[(read_addr_q-WavBase)>>2] :
|
||||||
|
((read_addr_q >= ModelBase) ? model[(read_addr_q-ModelBase)>>2] :
|
||||||
|
image[(read_addr_q-ImageBase)>>2]));
|
||||||
|
assign axi4.rresp = 2'd0;
|
||||||
|
assign axi4.rlast = read_beat_q == read_len_q;
|
||||||
|
assign axi4.ruser = 1'b0;
|
||||||
|
assign axi4.rvalid = read_active_q;
|
||||||
|
assign axi4.awready = 1'b1;
|
||||||
|
assign axi4.wready = 1'b0;
|
||||||
|
assign axi4.bid = 1'b0;
|
||||||
|
assign axi4.bresp = 2'd0;
|
||||||
|
assign axi4.buser = 1'b0;
|
||||||
|
assign axi4.bvalid = 1'b0;
|
||||||
|
|
||||||
|
assign dma_tx_axis.tdata = 32'd0;
|
||||||
|
assign dma_tx_axis.tkeep = 4'hf;
|
||||||
|
assign dma_tx_axis.tstrb = 4'hf;
|
||||||
|
assign dma_tx_axis.tlast = 1'b0;
|
||||||
|
assign dma_tx_axis.tid = '0;
|
||||||
|
assign dma_tx_axis.tdest = '0;
|
||||||
|
assign dma_tx_axis.tuser = '0;
|
||||||
|
assign dma_tx_axis.tvalid = 1'b0;
|
||||||
|
assign dma_rx_axis.tready = 1'b1;
|
||||||
|
|
||||||
|
assign i2s_tx_axis.tready = pb_count_q < 11'd1024;
|
||||||
|
|
||||||
|
assign i2s_rx_axis.tdata = rx_data_q;
|
||||||
|
assign i2s_rx_axis.tkeep = 4'hf;
|
||||||
|
assign i2s_rx_axis.tstrb = 4'hf;
|
||||||
|
assign i2s_rx_axis.tlast = 1'b0;
|
||||||
|
assign i2s_rx_axis.tid = '0;
|
||||||
|
assign i2s_rx_axis.tdest = '0;
|
||||||
|
assign i2s_rx_axis.tuser = '0;
|
||||||
|
assign i2s_rx_axis.tvalid = rx_valid_q;
|
||||||
|
|
||||||
|
function automatic logic [31:0] lfsr_step(input logic [31:0] value_i);
|
||||||
|
begin
|
||||||
|
lfsr_step = {value_i[30:0], value_i[31] ^ value_i[21] ^ value_i[1] ^ value_i[0]};
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
read_active_q <= 1'b0;
|
||||||
|
read_addr_q <= 32'd0;
|
||||||
|
read_len_q <= 8'd0;
|
||||||
|
read_beat_q <= 8'd0;
|
||||||
|
cycle_q <= 64'd0;
|
||||||
|
pb_count_q <= 11'd0;
|
||||||
|
pb_div_q <= 11'd0;
|
||||||
|
pb_started_q <= 1'b0;
|
||||||
|
pb_done_q <= 1'b0;
|
||||||
|
pb_xrun_q <= 1'b0;
|
||||||
|
pb_first_cycle_q <= 64'd0;
|
||||||
|
pb_last_cycle_q <= 64'd0;
|
||||||
|
pb_consumed_q <= 64'd0;
|
||||||
|
pb_underrun_q <= 32'd0;
|
||||||
|
rx_valid_q <= 1'b0;
|
||||||
|
rx_data_q <= 32'd0;
|
||||||
|
rx_lfsr_q <= seed_value | 32'd1;
|
||||||
|
rx_delivered_q <= 64'd0;
|
||||||
|
rx_presented_q <= 64'd0;
|
||||||
|
rx_overrun_q <= 32'd0;
|
||||||
|
rx_overrun_sticky_q <= 1'b0;
|
||||||
|
pat_arm_seen_q <= 1'b0;
|
||||||
|
pat_offset_q <= 15'd0;
|
||||||
|
tx_words_q <= 64'd0;
|
||||||
|
tx_tlast_q <= 64'd0;
|
||||||
|
tx_stall_cycles_q <= 64'd0;
|
||||||
|
rx_stall_cycles_q <= 64'd0;
|
||||||
|
dma_read_stall_cycles_q <= 64'd0;
|
||||||
|
dma_write_stall_cycles_q <= 64'd0;
|
||||||
|
end else begin
|
||||||
|
cycle_q <= cycle_q + 1'b1;
|
||||||
|
|
||||||
|
if (axi4.arvalid && axi4.arready) begin
|
||||||
|
read_active_q <= 1'b1;
|
||||||
|
read_addr_q <= axi4.araddr;
|
||||||
|
read_len_q <= axi4.arlen;
|
||||||
|
read_beat_q <= 8'd0;
|
||||||
|
end
|
||||||
|
if (axi4.rvalid && axi4.rready) begin
|
||||||
|
if (axi4.rlast) read_active_q <= 1'b0;
|
||||||
|
else begin
|
||||||
|
read_addr_q <= read_addr_q + 32'd4;
|
||||||
|
read_beat_q <= read_beat_q + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (axi4.awvalid && axi4.awready)
|
||||||
|
$fatal(1, "P7 concurrent unexpected AXI write %h", axi4.awaddr);
|
||||||
|
|
||||||
|
// Streamed-output scoreboard: byte-exact word pattern and TLAST checks.
|
||||||
|
if (pat_arm_q != pat_arm_seen_q) begin
|
||||||
|
pat_arm_seen_q <= pat_arm_q;
|
||||||
|
pat_offset_q <= 15'd0;
|
||||||
|
end else if (run_active_q && i2s_tx_axis.tvalid && i2s_tx_axis.tready) begin
|
||||||
|
if (i2s_tx_axis.tdata !== (pat_is_wav ? wav_pcm[pat_offset_q] : flac_pcm[pat_offset_q]))
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent PCM mismatch job=%0d offset=%0d data=%h expected=%h",
|
||||||
|
pat_is_wav,
|
||||||
|
pat_offset_q,
|
||||||
|
i2s_tx_axis.tdata,
|
||||||
|
pat_is_wav ? wav_pcm[pat_offset_q] : flac_pcm[pat_offset_q]
|
||||||
|
);
|
||||||
|
if (i2s_tx_axis.tlast !=
|
||||||
|
(pat_offset_q == (pat_is_wav ? 15'(wav_out_words) : 15'(flac_out_words)) - 15'd1))
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent TLAST mismatch job=%0d offset=%0d tlast=%0d",
|
||||||
|
pat_is_wav,
|
||||||
|
pat_offset_q,
|
||||||
|
i2s_tx_axis.tlast
|
||||||
|
);
|
||||||
|
pat_offset_q <= pat_offset_q + 1'b1;
|
||||||
|
tx_words_q <= tx_words_q + 1'b1;
|
||||||
|
if (i2s_tx_axis.tlast) tx_tlast_q <= tx_tlast_q + 1'b1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// I2S TX playback: start after the frozen 64-word prefill, then consume
|
||||||
|
// one word per frame tick; an empty FIFO at a tick is an underrun.
|
||||||
|
if (!pb_started_q && ((pb_count_q >= 11'd64) || (drain_q && (pb_count_q != 11'd0)))) begin
|
||||||
|
pb_started_q <= 1'b1;
|
||||||
|
pb_div_q <= frame_period - 1'b1;
|
||||||
|
end else if (pb_started_q && !pb_done_q) begin
|
||||||
|
if (pb_div_q + 1'b1 >= frame_period) begin
|
||||||
|
pb_div_q <= 11'd0;
|
||||||
|
if (pb_count_q == 11'd0) begin
|
||||||
|
if (drain_q) begin
|
||||||
|
pb_done_q <= 1'b1;
|
||||||
|
end else begin
|
||||||
|
pb_xrun_q <= 1'b1;
|
||||||
|
pb_underrun_q <= pb_underrun_q + 1'b1;
|
||||||
|
if (pb_underrun_q < 32'd16)
|
||||||
|
$display(
|
||||||
|
"CONCURRENT_XRUN cycle=%0d dut_fifo=%0d",
|
||||||
|
cycle_q - run_start_q,
|
||||||
|
u_dut.u_stream_router.s_tx_count
|
||||||
|
);
|
||||||
|
end
|
||||||
|
end else begin
|
||||||
|
if (pb_consumed_q == 64'd0) pb_first_cycle_q <= cycle_q;
|
||||||
|
pb_consumed_q <= pb_consumed_q + 1'b1;
|
||||||
|
pb_last_cycle_q <= cycle_q;
|
||||||
|
end
|
||||||
|
end else begin
|
||||||
|
pb_div_q <= pb_div_q + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
unique case ({
|
||||||
|
i2s_tx_axis.tvalid && i2s_tx_axis.tready,
|
||||||
|
pb_started_q && !pb_done_q && (pb_div_q + 1'b1 >= frame_period) && (pb_count_q != 11'd0)
|
||||||
|
})
|
||||||
|
2'b10: pb_count_q <= pb_count_q + 1'b1;
|
||||||
|
2'b01: pb_count_q <= pb_count_q - 1'b1;
|
||||||
|
default: begin
|
||||||
|
end
|
||||||
|
endcase
|
||||||
|
|
||||||
|
// I2S RX real-time feed: present each sample for exactly one cycle at the
|
||||||
|
// frame cadence; a sample not accepted at that instant is an overrun.
|
||||||
|
rx_valid_q <= 1'b0;
|
||||||
|
if (rx_feed_q && ((run_start_q + rx_presented_q * {53'd0, frame_period}) <
|
||||||
|
feed_stop_cycle_q) &&
|
||||||
|
(cycle_q >= run_start_q + rx_presented_q * {53'd0, frame_period})) begin
|
||||||
|
rx_valid_q <= 1'b1;
|
||||||
|
rx_data_q <= rx_lfsr_q;
|
||||||
|
rx_lfsr_q <= lfsr_step(rx_lfsr_q);
|
||||||
|
rx_presented_q <= rx_presented_q + 1'b1;
|
||||||
|
end
|
||||||
|
if (i2s_rx_axis.tvalid && i2s_rx_axis.tready) begin
|
||||||
|
rx_delivered_q <= rx_delivered_q + 1'b1;
|
||||||
|
end else if (i2s_rx_axis.tvalid && !i2s_rx_axis.tready) begin
|
||||||
|
rx_overrun_q <= rx_overrun_q + 1'b1;
|
||||||
|
rx_overrun_sticky_q <= 1'b1;
|
||||||
|
end
|
||||||
|
|
||||||
|
if (perf_count_q) begin
|
||||||
|
if (i2s_tx_axis.tvalid && !i2s_tx_axis.tready)
|
||||||
|
tx_stall_cycles_q <= tx_stall_cycles_q + 1'b1;
|
||||||
|
if (i2s_rx_axis.tvalid && !i2s_rx_axis.tready)
|
||||||
|
rx_stall_cycles_q <= rx_stall_cycles_q + 1'b1;
|
||||||
|
// Mirror apu_dma's stall accounting: pending request without any AXI
|
||||||
|
// handshake progress in the same cycle.
|
||||||
|
if (u_dut.u_apu_dma.input_pending_o &&
|
||||||
|
!((axi4.awvalid && axi4.awready) || (axi4.wvalid && axi4.wready) ||
|
||||||
|
(axi4.bvalid && axi4.bready) || (axi4.arvalid && axi4.arready) ||
|
||||||
|
(axi4.rvalid && axi4.rready)))
|
||||||
|
dma_read_stall_cycles_q <= dma_read_stall_cycles_q + 1'b1;
|
||||||
|
if (u_dut.u_apu_dma.output_pending_o &&
|
||||||
|
!((axi4.awvalid && axi4.awready) || (axi4.wvalid && axi4.wready) ||
|
||||||
|
(axi4.bvalid && axi4.bready) || (axi4.arvalid && axi4.arready) ||
|
||||||
|
(axi4.rvalid && axi4.rready)))
|
||||||
|
dma_write_stall_cycles_q <= dma_write_stall_cycles_q + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
apb4_apu #(
|
||||||
|
.EnableP7(1'b1)
|
||||||
|
) u_dut (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.owner_i (2'd0),
|
||||||
|
.owner_lock_i (1'b0),
|
||||||
|
.quiesce_i (apb4.pprot[0]),
|
||||||
|
.resource_reset_i,
|
||||||
|
.bridge_epoch_i (8'd0),
|
||||||
|
.i2s_tx_underrun_i (pb_xrun_q),
|
||||||
|
.i2s_rx_overrun_i (rx_overrun_sticky_q),
|
||||||
|
.i2s_rx_flush_busy_i(1'b0),
|
||||||
|
.apb4,
|
||||||
|
.axi4,
|
||||||
|
.dma_tx_axis,
|
||||||
|
.dma_rx_axis,
|
||||||
|
.i2s_tx_axis,
|
||||||
|
.i2s_rx_axis,
|
||||||
|
.idle_o (),
|
||||||
|
.irq_o ()
|
||||||
|
);
|
||||||
|
|
||||||
|
task automatic apb_write(input logic [11:0] offset_i, input logic [31:0] value_i);
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.paddr = ApuBase + offset_i;
|
||||||
|
apb4.psel = 1'b1;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b1;
|
||||||
|
apb4.pwdata = value_i;
|
||||||
|
apb4.pstrb = 4'hf;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.penable = 1'b1;
|
||||||
|
while (!apb4.pready) @(negedge clk_i);
|
||||||
|
if (apb4.pslverr)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent APB write %h = %h failed codec=%0d kws=%h",
|
||||||
|
offset_i,
|
||||||
|
value_i,
|
||||||
|
u_dut.u_codec_controller.s_state_q,
|
||||||
|
u_dut.s_kws_status
|
||||||
|
);
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic apb_read(input logic [11:0] offset_i, output logic [31:0] value_o);
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.paddr = ApuBase + offset_i;
|
||||||
|
apb4.psel = 1'b1;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b0;
|
||||||
|
apb4.pstrb = 4'd0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.penable = 1'b1;
|
||||||
|
while (!apb4.pready) @(negedge clk_i);
|
||||||
|
if (apb4.pslverr) $fatal(1, "P7 concurrent APB read failed %h", offset_i);
|
||||||
|
value_o = apb4.prdata;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic run_job(input logic is_wav_i, output logic [31:0] input_used_o,
|
||||||
|
output logic [31:0] output_bytes_o, output logic [31:0] frames_o,
|
||||||
|
output logic [31:0] cycles_o);
|
||||||
|
logic seen_busy;
|
||||||
|
logic [63:0] job_start;
|
||||||
|
begin
|
||||||
|
pat_is_wav = is_wav_i;
|
||||||
|
pat_arm_q = !pat_arm_q;
|
||||||
|
if (is_wav_i) begin
|
||||||
|
apb_write(`APB4_APU__JOB_CONTROL, wav_control);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_ADDRESS, WavBase);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_LENGTH, wav_bytes);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_CONFIG, wav_input_config);
|
||||||
|
end else begin
|
||||||
|
apb_write(`APB4_APU__JOB_CONTROL, 32'h0000_0120);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_ADDRESS, FlacBase);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_LENGTH, flac_bytes);
|
||||||
|
apb_write(`APB4_APU__JOB_INPUT_CONFIG, 32'd0);
|
||||||
|
end
|
||||||
|
apb_write(`APB4_APU__JOB_OUTPUT_ADDRESS, 32'd0);
|
||||||
|
apb_write(`APB4_APU__JOB_OUTPUT_CAPACITY, 32'd0);
|
||||||
|
apb_write(`APB4_APU__JOB_OUTPUT_CONFIG, output_config);
|
||||||
|
apb_write(`APB4_APU__JOB_FLAGS, 32'd1);
|
||||||
|
job_start = cycle_q;
|
||||||
|
apb_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_START_DIRECT);
|
||||||
|
seen_busy = 1'b0;
|
||||||
|
for (int poll = 0; poll < 25000000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__JOB_STATUS, s_value);
|
||||||
|
if (s_value[`APB4_APU__JOB_STATUS_BUSY]) seen_busy = 1'b1;
|
||||||
|
if (seen_busy && (s_value[`APB4_APU__JOB_STATUS_DONE] ||
|
||||||
|
s_value[`APB4_APU__JOB_STATUS_ERROR]))
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
if (!s_value[`APB4_APU__JOB_STATUS_DONE]) begin
|
||||||
|
$display("P7 concurrent job failed is_wav=%0d status=%h seq=%h retired=%h fault=%h",
|
||||||
|
is_wav_i, s_value, u_dut.s_seq_status, u_dut.s_seq_retired,
|
||||||
|
u_dut.s_seq_fault_detail);
|
||||||
|
$display("P7 concurrent codec detail=%h transport=%0d dma_busy=%0d stream_idle=%0d",
|
||||||
|
u_dut.s_codec_detail, u_dut.u_codec_transport.s_state_q, u_dut.s_dma_busy,
|
||||||
|
u_dut.s_stream_idle);
|
||||||
|
apb_read(`APB4_APU__ERROR_STATUS, s_value);
|
||||||
|
$display("P7 concurrent error status=%h", s_value);
|
||||||
|
apb_read(`APB4_APU__ERROR_ADDRESS, s_value);
|
||||||
|
$display("P7 concurrent error address=%h", s_value);
|
||||||
|
apb_read(`APB4_APU__ERROR_DETAIL, s_value);
|
||||||
|
$fatal(1, "P7 concurrent job error detail=%h", s_value);
|
||||||
|
end
|
||||||
|
apb_read(`APB4_APU__JOB_INPUT_USED, input_used_o);
|
||||||
|
if (input_used_o != (is_wav_i ? wav_bytes : flac_bytes))
|
||||||
|
$fatal(1, "P7 concurrent input count mismatch is_wav=%0d used=%0d", is_wav_i, input_used_o);
|
||||||
|
apb_read(`APB4_APU__JOB_OUTPUT_BYTES, output_bytes_o);
|
||||||
|
if (output_bytes_o != (is_wav_i ? (wav_out_words << 2) : (flac_out_words << 2)))
|
||||||
|
$fatal(
|
||||||
|
1, "P7 concurrent output count mismatch is_wav=%0d bytes=%0d", is_wav_i, output_bytes_o
|
||||||
|
);
|
||||||
|
apb_read(`APB4_APU__JOB_FRAMES, frames_o);
|
||||||
|
if (frames_o != (is_wav_i ? wav_frames : flac_frames))
|
||||||
|
$fatal(1, "P7 concurrent frame count mismatch is_wav=%0d frames=%0d", is_wav_i, frames_o);
|
||||||
|
apb_read(`APB4_APU__JOB_CYCLES, cycles_o);
|
||||||
|
if (cycles_o == 32'd0) $fatal(1, "P7 concurrent stale job cycle count");
|
||||||
|
if (job_log)
|
||||||
|
$display(
|
||||||
|
"CONCURRENT_JOB is_wav=%0d cycles=%0d wall=%0d", is_wav_i, cycles_o, cycle_q - job_start
|
||||||
|
);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
logic is_wav;
|
||||||
|
logic [63:0] job_count, wav_jobs, flac_jobs;
|
||||||
|
logic [63:0] codec_input_bytes, codec_output_bytes, codec_frames, codec_cycles;
|
||||||
|
logic [63:0] kws_expected_16k, kws_expected_frames, kws_expected_windows;
|
||||||
|
logic [63:0] kws_frames_read, kws_windows_read;
|
||||||
|
logic [63:0] perf_input_bytes, perf_output_bytes, perf_active_cycles;
|
||||||
|
logic [63:0] perf_read_stalls, perf_write_stalls, perf_stream_stalls, perf_faults;
|
||||||
|
logic [31:0] kws_overrun_read, kws_status_read;
|
||||||
|
logic [31:0] job_iused, job_obytes, job_fcount, job_cyc;
|
||||||
|
integer drain_poll;
|
||||||
|
|
||||||
|
wav_bytes = 32'd0;
|
||||||
|
wav_frames = 32'd0;
|
||||||
|
wav_out_words = 32'd0;
|
||||||
|
flac_bytes = 32'd0;
|
||||||
|
flac_frames = 32'd0;
|
||||||
|
flac_out_words = 32'd0;
|
||||||
|
rate_hz = 32'd0;
|
||||||
|
precision_bits = 32'd0;
|
||||||
|
seconds_ms = 32'd0;
|
||||||
|
duration_cycles = 64'd0;
|
||||||
|
max_cycles = 64'd0;
|
||||||
|
seed_value = 32'd0;
|
||||||
|
workload_mask = 32'd0;
|
||||||
|
job_log = 32'd0;
|
||||||
|
wav_source_rate = 32'd0;
|
||||||
|
if (!$value$plusargs("APUMC_HEX=%s", apumc_path)) $fatal(1, "APUMC_HEX plusarg missing");
|
||||||
|
if (!$value$plusargs("APUM_HEX=%s", apum_path)) $fatal(1, "APUM_HEX plusarg missing");
|
||||||
|
if (!$value$plusargs("WAV_HEX=%s", wav_path)) $fatal(1, "WAV_HEX plusarg missing");
|
||||||
|
if (!$value$plusargs("FLAC_HEX=%s", flac_path)) $fatal(1, "FLAC_HEX plusarg missing");
|
||||||
|
if (!$value$plusargs("WAV_PCM_HEX=%s", wav_pcm_path)) $fatal(1, "WAV_PCM_HEX missing");
|
||||||
|
if (!$value$plusargs("FLAC_PCM_HEX=%s", flac_pcm_path)) $fatal(1, "FLAC_PCM_HEX missing");
|
||||||
|
if (!$value$plusargs("WAV_BYTES=%0d", wav_bytes)) $fatal(1, "WAV_BYTES missing");
|
||||||
|
if (!$value$plusargs("WAV_FRAMES=%0d", wav_frames)) $fatal(1, "WAV_FRAMES missing");
|
||||||
|
if (!$value$plusargs("WAV_OUT_WORDS=%0d", wav_out_words)) $fatal(1, "WAV_OUT_WORDS missing");
|
||||||
|
if (!$value$plusargs("FLAC_BYTES=%0d", flac_bytes)) $fatal(1, "FLAC_BYTES missing");
|
||||||
|
if (!$value$plusargs("FLAC_FRAMES=%0d", flac_frames)) $fatal(1, "FLAC_FRAMES missing");
|
||||||
|
if (!$value$plusargs("FLAC_OUT_WORDS=%0d", flac_out_words)) $fatal(1, "FLAC_OUT_WORDS missing");
|
||||||
|
if (!$value$plusargs("RATE=%0d", rate_hz)) $fatal(1, "RATE missing");
|
||||||
|
if (!$value$plusargs("PRECISION=%0d", precision_bits)) $fatal(1, "PRECISION missing");
|
||||||
|
if (!$value$plusargs("DURATION_CYCLES=%0d", duration_cycles))
|
||||||
|
$fatal(1, "DURATION_CYCLES missing");
|
||||||
|
if (!$value$plusargs("SECONDS_MS=%0d", seconds_ms)) $fatal(1, "SECONDS_MS missing");
|
||||||
|
if (!$value$plusargs("MAX_CYCLES=%0d", max_cycles)) $fatal(1, "MAX_CYCLES missing");
|
||||||
|
if (!$value$plusargs("SEED=%0d", seed_value)) seed_value = 32'd1;
|
||||||
|
if (!$value$plusargs("WORKLOAD_MASK=%0d", workload_mask)) workload_mask = 32'd3;
|
||||||
|
if (!$value$plusargs("JOB_LOG=%0d", job_log)) job_log = 32'd0;
|
||||||
|
if (!$value$plusargs("WAV_SOURCE_RATE=%0d", wav_source_rate)) wav_source_rate = rate_hz;
|
||||||
|
if ((rate_hz != 32'd48000) && (rate_hz != 32'd96000)) $fatal(1, "RATE must be 48000/96000");
|
||||||
|
if ((precision_bits != 32'd16) && (precision_bits != 32'd24))
|
||||||
|
$fatal(1, "PRECISION must be 16/24");
|
||||||
|
if ((workload_mask == 32'd0) || (workload_mask > 32'd3)) $fatal(1, "WORKLOAD_MASK invalid");
|
||||||
|
if ((wav_out_words > 15'd16384) || (flac_out_words > 15'd16384))
|
||||||
|
$fatal(1, "workload output words exceed the pattern storage");
|
||||||
|
|
||||||
|
$readmemh(apumc_path, image);
|
||||||
|
$readmemh(apum_path, model);
|
||||||
|
$readmemh(wav_path, wav_image);
|
||||||
|
$readmemh(flac_path, flac_image);
|
||||||
|
$readmemh(wav_pcm_path, wav_pcm);
|
||||||
|
$readmemh(flac_pcm_path, flac_pcm);
|
||||||
|
|
||||||
|
output_config = ((precision_bits == 32'd24) ? (32'd1 << 19) : 32'd0) | (32'd2 << 17) | rate_hz;
|
||||||
|
wav_input_config = (32'd16 << 20) | (32'd2 << 17) | wav_source_rate;
|
||||||
|
wav_control = (wav_source_rate == rate_hz) ? 32'h0000_0100 : 32'h0000_0900;
|
||||||
|
kws_input_config = ((precision_bits == 32'd24) ? (32'd24 << 20) : (32'd16 << 20)) |
|
||||||
|
(32'd2 << 17) | rate_hz;
|
||||||
|
rx_words_per_frame = (precision_bits == 32'd24) ? 2'd2 : 2'd1;
|
||||||
|
frame_period = 11'(32'(48000000) / rate_hz / {30'd0, rx_words_per_frame});
|
||||||
|
decimation = (rate_hz == 32'd96000) ? 3'd6 : 3'd3;
|
||||||
|
|
||||||
|
apb4.paddr = 32'd0;
|
||||||
|
resource_reset_i = 1'b0;
|
||||||
|
apb4.pprot = 3'd1;
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b0;
|
||||||
|
apb4.pwdata = 32'd0;
|
||||||
|
apb4.pstrb = 4'd0;
|
||||||
|
drain_q = 1'b0;
|
||||||
|
run_active_q = 1'b0;
|
||||||
|
perf_count_q = 1'b0;
|
||||||
|
rx_feed_q = 1'b0;
|
||||||
|
feed_stop_cycle_q = 64'hffff_ffff_ffff_ffff;
|
||||||
|
pat_is_wav = 1'b1;
|
||||||
|
pat_arm_q = 1'b0;
|
||||||
|
repeat (4) @(posedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
repeat (2) @(posedge clk_i);
|
||||||
|
|
||||||
|
apb_write(`APB4_APU__READ_BASE, ImageBase);
|
||||||
|
apb_write(`APB4_APU__READ_LIMIT, FlacBase + 32'd4095);
|
||||||
|
apb_write(`APB4_APU__WRITE_BASE, WriteBase);
|
||||||
|
apb_write(`APB4_APU__WRITE_LIMIT, WriteBase + 32'd255);
|
||||||
|
apb_write(`APB4_APU__MC_IMAGE_ADDRESS, ImageBase);
|
||||||
|
apb_write(`APB4_APU__MC_IMAGE_SIZE, image[2]);
|
||||||
|
apb_write(`APB4_APU__MC_EXPECTED_CRC, image[11]);
|
||||||
|
apb_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MICROCODE_LOAD);
|
||||||
|
for (int poll = 0; poll < 500000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__MC_STATUS, s_value);
|
||||||
|
if (s_value[`APB4_APU__MC_STATUS_VALID]) break;
|
||||||
|
if (!s_value[`APB4_APU__MC_STATUS_BUSY])
|
||||||
|
$fatal(1, "P7 concurrent microcode load failed %h", s_value);
|
||||||
|
end
|
||||||
|
if (!s_value[`APB4_APU__MC_STATUS_VALID]) $fatal(1, "P7 concurrent microcode load timeout");
|
||||||
|
$display("CONCURRENT_SETUP microcode loaded crc=%h", image[11]);
|
||||||
|
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_ADDRESS, ModelBase);
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_SIZE, 32'd32768);
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_EXPECTED_CRC, `APB4_APU__APUM_PAYLOAD_CRC);
|
||||||
|
apb_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MODEL_LOAD);
|
||||||
|
// The model load is admitted under quiesce, but its DMA only runs once
|
||||||
|
// quiesce deasserts (apu_dma blocks new requests while quiesced and the
|
||||||
|
// microcode loader is idle).
|
||||||
|
apb4.pprot = 3'd0;
|
||||||
|
for (int poll = 0; poll < 500000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__KWS_MODEL_STATUS, s_value);
|
||||||
|
if (!s_value[0]) break;
|
||||||
|
end
|
||||||
|
if (s_value != 32'h0000_0006) begin
|
||||||
|
apb_read(`APB4_APU__ERROR_STATUS, s_value);
|
||||||
|
$display("P7 concurrent model load error status=%h", s_value);
|
||||||
|
apb_read(`APB4_APU__ERROR_DETAIL, s_value);
|
||||||
|
$fatal(1, "P7 concurrent model load failed detail=%h", s_value);
|
||||||
|
end
|
||||||
|
$display("CONCURRENT_SETUP model loaded status=%h", s_value);
|
||||||
|
|
||||||
|
apb_write(`APB4_APU__KWS_INPUT_CONFIG, kws_input_config);
|
||||||
|
apb_write(`APB4_APU__KWS_CONFIG, 32'h0000_0180);
|
||||||
|
apb_write(`APB4_APU__STREAM_ROUTE, 32'd5);
|
||||||
|
apb_write(`APB4_APU__PERF_CONTROL, 32'h3);
|
||||||
|
perf_count_q = 1'b1;
|
||||||
|
apb_write(`APB4_APU__KWS_CONTROL, 32'd1);
|
||||||
|
repeat (32) @(posedge clk_i);
|
||||||
|
apb_read(`APB4_APU__KWS_STATUS, s_value);
|
||||||
|
if (!s_value[`APB4_APU__KWS_STATUS_LISTENING]) $fatal(1, "P7 concurrent KWS not listening");
|
||||||
|
|
||||||
|
run_start_q = cycle_q;
|
||||||
|
feed_stop_cycle_q = cycle_q + duration_cycles;
|
||||||
|
rx_feed_q = 1'b1;
|
||||||
|
run_active_q = 1'b1;
|
||||||
|
job_count = 64'd0;
|
||||||
|
wav_jobs = 64'd0;
|
||||||
|
flac_jobs = 64'd0;
|
||||||
|
codec_input_bytes = 64'd0;
|
||||||
|
codec_output_bytes = 64'd0;
|
||||||
|
codec_frames = 64'd0;
|
||||||
|
codec_cycles = 64'd0;
|
||||||
|
while (cycle_q < run_start_q + duration_cycles) begin
|
||||||
|
// The constant-FLAC workload is the buffer-filling one: running it
|
||||||
|
// first charges the playback buffer before the tighter WAV jobs,
|
||||||
|
// exactly the role of the frozen 64-word prefill discipline.
|
||||||
|
if (workload_mask == 32'd3) is_wav = (job_count[0] == 1'b1);
|
||||||
|
else is_wav = workload_mask[0];
|
||||||
|
run_job(is_wav, job_iused, job_obytes, job_fcount, job_cyc);
|
||||||
|
codec_input_bytes = codec_input_bytes + {32'd0, job_iused};
|
||||||
|
codec_output_bytes = codec_output_bytes + {32'd0, job_obytes};
|
||||||
|
codec_frames = codec_frames + {32'd0, job_fcount};
|
||||||
|
codec_cycles = codec_cycles + {32'd0, job_cyc};
|
||||||
|
if (is_wav) wav_jobs = wav_jobs + 1'b1;
|
||||||
|
else flac_jobs = flac_jobs + 1'b1;
|
||||||
|
job_count = job_count + 1'b1;
|
||||||
|
if ((job_count % 64) == 0)
|
||||||
|
$display("CONCURRENT_PROGRESS jobs=%0d cycle=%0d", job_count, cycle_q - run_start_q);
|
||||||
|
end
|
||||||
|
drain_q = 1'b1;
|
||||||
|
drain_poll = 0;
|
||||||
|
while (!pb_done_q && (drain_poll < 100000000)) begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
drain_poll = drain_poll + 1;
|
||||||
|
end
|
||||||
|
if (!pb_done_q) $fatal(1, "P7 concurrent playback drain timeout count=%0d", pb_count_q);
|
||||||
|
rx_feed_q = 1'b0;
|
||||||
|
|
||||||
|
// KWS schedule scoreboard: derive the exact expected frame/window counts
|
||||||
|
// from the delivered uninterrupted sample stream.
|
||||||
|
kws_expected_16k = rx_delivered_q / {62'd0, rx_words_per_frame} / {61'd0, decimation};
|
||||||
|
kws_expected_frames = (kws_expected_16k >= 64'd480) ?
|
||||||
|
((kws_expected_16k - 64'd480) / 64'd320) + 64'd1 : 64'd0;
|
||||||
|
kws_expected_windows = (kws_expected_16k >= 64'd16000) ?
|
||||||
|
((kws_expected_16k - 64'd16000) / 64'd1600) + 64'd1 : 64'd0;
|
||||||
|
for (int poll = 0; poll < 100000000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__KWS_FRAME_COUNT, s_value);
|
||||||
|
kws_frames_read = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__KWS_INFERENCE_COUNT, s_value);
|
||||||
|
kws_windows_read = {32'd0, s_value};
|
||||||
|
if ((kws_frames_read == kws_expected_frames) && (kws_windows_read == kws_expected_windows))
|
||||||
|
break;
|
||||||
|
if (poll == 99999999) begin
|
||||||
|
$display(
|
||||||
|
"P7 concurrent KWS schedule timeout frames=%0d/%0d windows=%0d/%0d epoch=%0d front=%0d infer=%0d",
|
||||||
|
kws_frames_read, kws_expected_frames, kws_windows_read, kws_expected_windows,
|
||||||
|
u_dut.u_kws_engine.s_epoch_samples_q, u_dut.u_kws_engine.s_front_state_q,
|
||||||
|
u_dut.u_kws_engine.s_infer_state_q);
|
||||||
|
$fatal(1, "P7 concurrent KWS schedule mismatch");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
apb_read(`APB4_APU__KWS_OVERRUN_COUNT, kws_overrun_read);
|
||||||
|
apb_read(`APB4_APU__KWS_STATUS, kws_status_read);
|
||||||
|
apb_write(`APB4_APU__PERF_CONTROL, 32'h5);
|
||||||
|
perf_count_q = 1'b0;
|
||||||
|
apb_read(`APB4_APU__PERF_STATUS, s_value);
|
||||||
|
if (!s_value[`APB4_APU__PERF_STATUS_SNAPSHOT_VALID]) $fatal(1, "P7 perf snapshot missing");
|
||||||
|
apb_read(`APB4_APU__PERF_ACTIVE_CYCLES_LO, s_value);
|
||||||
|
perf_active_cycles = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_ACTIVE_CYCLES_HI, s_value);
|
||||||
|
perf_active_cycles = perf_active_cycles | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_INPUT_BYTES_LO, s_value);
|
||||||
|
perf_input_bytes = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_INPUT_BYTES_HI, s_value);
|
||||||
|
perf_input_bytes = perf_input_bytes | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_OUTPUT_BYTES_LO, s_value);
|
||||||
|
perf_output_bytes = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_OUTPUT_BYTES_HI, s_value);
|
||||||
|
perf_output_bytes = perf_output_bytes | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_DMA_READ_STALLS_LO, s_value);
|
||||||
|
perf_read_stalls = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_DMA_READ_STALLS_HI, s_value);
|
||||||
|
perf_read_stalls = perf_read_stalls | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_DMA_WRITE_STALLS_LO, s_value);
|
||||||
|
perf_write_stalls = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_DMA_WRITE_STALLS_HI, s_value);
|
||||||
|
perf_write_stalls = perf_write_stalls | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_STREAM_STALLS_LO, s_value);
|
||||||
|
perf_stream_stalls = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_STREAM_STALLS_HI, s_value);
|
||||||
|
perf_stream_stalls = perf_stream_stalls | ({32'd0, s_value} << 32);
|
||||||
|
apb_read(`APB4_APU__PERF_FAULTS_LO, s_value);
|
||||||
|
perf_faults = {32'd0, s_value};
|
||||||
|
apb_read(`APB4_APU__PERF_FAULTS_HI, s_value);
|
||||||
|
perf_faults = perf_faults | ({32'd0, s_value} << 32);
|
||||||
|
|
||||||
|
// Frozen zero-xrun gate and byte/frame/cycle/stall scoreboards.
|
||||||
|
if (pb_underrun_q != 32'd0) $fatal(1, "P7 concurrent codec underrun count=%0d", pb_underrun_q);
|
||||||
|
if (rx_overrun_q != 32'd0) $fatal(1, "P7 concurrent I2S RX overrun count=%0d", rx_overrun_q);
|
||||||
|
if (kws_overrun_read != 32'd0 || kws_status_read[`APB4_APU__KWS_STATUS_OVERRUN])
|
||||||
|
$fatal(1, "P7 concurrent KWS overrun count=%0d status=%h", kws_overrun_read, kws_status_read);
|
||||||
|
if ((kws_frames_read != kws_expected_frames) || (kws_windows_read != kws_expected_windows))
|
||||||
|
$fatal(1, "P7 concurrent KWS window schedule mismatch");
|
||||||
|
if (tx_words_q != codec_output_bytes >> 2)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent stream word scoreboard mismatch words=%0d bytes=%0d",
|
||||||
|
tx_words_q,
|
||||||
|
codec_output_bytes
|
||||||
|
);
|
||||||
|
if (tx_tlast_q != job_count)
|
||||||
|
$fatal(
|
||||||
|
1, "P7 concurrent TLAST scoreboard mismatch tlast=%0d jobs=%0d", tx_tlast_q, job_count
|
||||||
|
);
|
||||||
|
if (pb_consumed_q != tx_words_q)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent playback consumption mismatch consumed=%0d words=%0d",
|
||||||
|
pb_consumed_q,
|
||||||
|
tx_words_q
|
||||||
|
);
|
||||||
|
if ((pb_consumed_q > 64'd1) &&
|
||||||
|
(pb_last_cycle_q - pb_first_cycle_q != (pb_consumed_q - 64'd1) * {53'd0, frame_period}))
|
||||||
|
$fatal(
|
||||||
|
1, "P7 concurrent playback span mismatch span=%0d", pb_last_cycle_q - pb_first_cycle_q
|
||||||
|
);
|
||||||
|
if ((wav_jobs == 64'd0) && workload_mask[0]) $fatal(1, "P7 concurrent WAV workload absent");
|
||||||
|
if ((flac_jobs == 64'd0) && workload_mask[1]) $fatal(1, "P7 concurrent FLAC workload absent");
|
||||||
|
if (perf_input_bytes != codec_input_bytes)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent PERF input byte scoreboard mismatch perf=%0d codec=%0d",
|
||||||
|
perf_input_bytes,
|
||||||
|
codec_input_bytes
|
||||||
|
);
|
||||||
|
if (perf_output_bytes != 64'd0)
|
||||||
|
$fatal(1, "P7 concurrent PERF output byte scoreboard mismatch %0d", perf_output_bytes);
|
||||||
|
if (perf_read_stalls != dma_read_stall_cycles_q)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent DMA read stall scoreboard mismatch perf=%0d tb=%0d",
|
||||||
|
perf_read_stalls,
|
||||||
|
dma_read_stall_cycles_q
|
||||||
|
);
|
||||||
|
if (perf_write_stalls != dma_write_stall_cycles_q)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent DMA write stall scoreboard mismatch perf=%0d tb=%0d",
|
||||||
|
perf_write_stalls,
|
||||||
|
dma_write_stall_cycles_q
|
||||||
|
);
|
||||||
|
if (perf_stream_stalls != tx_stall_cycles_q + rx_stall_cycles_q)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P7 concurrent stream stall scoreboard mismatch perf=%0d tb=%0d",
|
||||||
|
perf_stream_stalls,
|
||||||
|
tx_stall_cycles_q + rx_stall_cycles_q
|
||||||
|
);
|
||||||
|
if (perf_faults != 64'd0) $fatal(1, "P7 concurrent PERF fault scoreboard mismatch");
|
||||||
|
if ((perf_active_cycles == 64'd0) || (perf_active_cycles > cycle_q - run_start_q))
|
||||||
|
$fatal(1, "P7 concurrent PERF active cycle scoreboard mismatch %0d", perf_active_cycles);
|
||||||
|
apb_read(`APB4_APU__ERROR_STATUS, s_value);
|
||||||
|
if (s_value != 32'd0) $fatal(1, "P7 concurrent first-error status set %h", s_value);
|
||||||
|
|
||||||
|
$display(
|
||||||
|
"CONCURRENT_RESULT rate=%0d precision=%0d seconds_ms=%0d duration_cycles=%0d jobs=%0d wav_jobs=%0d flac_jobs=%0d tx_words=%0d consumed=%0d underrun=%0d rx_overrun=%0d kws_overrun=%0d kws_frames=%0d kws_windows=%0d rx_words=%0d codec_input_bytes=%0d codec_output_bytes=%0d codec_frames=%0d codec_cycles=%0d dma_read_stalls=%0d dma_write_stalls=%0d stream_stalls=%0d active_cycles=%0d elapsed_cycles=%0d scoreboard=match",
|
||||||
|
rate_hz, precision_bits, seconds_ms, duration_cycles, job_count, wav_jobs, flac_jobs,
|
||||||
|
tx_words_q, pb_consumed_q, pb_underrun_q, rx_overrun_q, kws_overrun_read, kws_frames_read,
|
||||||
|
kws_windows_read, rx_delivered_q, codec_input_bytes, codec_output_bytes, codec_frames,
|
||||||
|
codec_cycles, perf_read_stalls, perf_write_stalls, perf_stream_stalls, perf_active_cycles,
|
||||||
|
cycle_q - run_start_q);
|
||||||
|
$display("APU-P7 concurrent scenario passed");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
wait (cycle_q > max_cycles);
|
||||||
|
$fatal(1, "P7 concurrent global timeout");
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
// SPDX-License-Identifier: MulanPSL-2.0
|
||||||
|
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// APU-P7 KWS frontend differential fixture. Each window is a plusarg-selected
|
||||||
|
// 32000-byte padded PCM image driven through operation 1 (one KWS memory
|
||||||
|
// window) on the real frontend/inference RTL path with the frozen APUM model
|
||||||
|
// resident in the KWS SRAM model reservation, exactly like the accuracy
|
||||||
|
// fixture. After each completed window the fixture prints the 490 computed
|
||||||
|
// MFCC scratch bytes (MFCC_HEX) and the window normalization peak (PEAK) for
|
||||||
|
// the host-side bit-accurate model comparison. Classifier history and
|
||||||
|
// counters are reset at every idle boundary between windows. The fixture
|
||||||
|
// fails closed on any DMA protocol, error, or timeout condition.
|
||||||
|
module apu_p7_frontend_dump_tb;
|
||||||
|
localparam int unsigned PcmWords = 8000;
|
||||||
|
localparam int unsigned ModelWords = 8192;
|
||||||
|
localparam logic [15:0] KwsConfig = 16'h0180; // debounce=1, threshold=128
|
||||||
|
localparam logic [31:0] InputConfig = 32'h0102_3e80; // mono/16000/16 S16_LE
|
||||||
|
localparam logic [31:0] MemoryBase = 32'h0001_0000;
|
||||||
|
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic clear_history = 1'b0;
|
||||||
|
logic counter_clear = 1'b0;
|
||||||
|
logic memory_start = 1'b0;
|
||||||
|
logic memory_dma_request_ready = 1'b0;
|
||||||
|
logic [31:0] memory_dma_data = 32'd0;
|
||||||
|
logic [ 3:0] memory_dma_keep = 4'd0;
|
||||||
|
logic memory_dma_last = 1'b0;
|
||||||
|
logic memory_dma_valid = 1'b0;
|
||||||
|
logic memory_dma_done = 1'b0;
|
||||||
|
logic [31:0] status_o, result_o, frame_count_o, inference_count_o, hit_count_o;
|
||||||
|
logic [31:0] overrun_count_o, timestamp_lo_o, timestamp_hi_o;
|
||||||
|
logic [31:0] model_status_o, model_actual_crc_o, memory_input_used_o;
|
||||||
|
logic memory_start_ready_o;
|
||||||
|
logic memory_dma_request_valid_o, memory_done_o, memory_error_o;
|
||||||
|
logic [31:0] memory_dma_request_address_o, memory_dma_request_bytes_o;
|
||||||
|
logic [5:0] memory_error_code_o;
|
||||||
|
logic [3:0] memory_error_stage_o;
|
||||||
|
logic [1:0] memory_error_resp_o;
|
||||||
|
logic [31:0] memory_error_address_o, memory_error_detail_o;
|
||||||
|
logic [10:0] irq_set_o;
|
||||||
|
logic fault_valid_o, model_valid_o, model_lock_o, busy_o, idle_o, rx_ready_o;
|
||||||
|
logic [5:0] fault_code_o;
|
||||||
|
logic [3:0] fault_stage_o;
|
||||||
|
logic [1:0] fault_resp_o;
|
||||||
|
logic [7:0] fault_index_o;
|
||||||
|
logic [31:0] fault_addr_o, fault_detail_o;
|
||||||
|
logic [15:0][14:0] model_addr_o;
|
||||||
|
logic [15:0][ 7:0] model_store_data;
|
||||||
|
logic scratch_clear;
|
||||||
|
logic [19:0][15:0] scratch_read_addr;
|
||||||
|
logic [19:0][31:0] scratch_read_data;
|
||||||
|
logic [ 5:0] scratch_write_valid;
|
||||||
|
logic [ 5:0][15:0] scratch_write_addr;
|
||||||
|
logic [ 5:0][31:0] scratch_write_data;
|
||||||
|
logic [ 5:0][ 3:0] scratch_write_strb;
|
||||||
|
logic scratch_access_err;
|
||||||
|
logic [31:0] pcm_mem [0:PcmWords-1];
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) stream_i (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
assign stream_i.tvalid = 1'b0;
|
||||||
|
assign stream_i.tdata = 32'd0;
|
||||||
|
assign stream_i.tkeep = 4'hf;
|
||||||
|
assign stream_i.tstrb = 4'hf;
|
||||||
|
assign stream_i.tlast = 1'b0;
|
||||||
|
assign stream_i.tid = '0;
|
||||||
|
assign stream_i.tdest = '0;
|
||||||
|
assign stream_i.tuser = '0;
|
||||||
|
assign stream_i.tready = rx_ready_o;
|
||||||
|
|
||||||
|
apu_kws_sram_client u_kws_sram_client (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i (rst_n_i),
|
||||||
|
.req_i (1'b0),
|
||||||
|
.write_i (1'b0),
|
||||||
|
.addr_i (17'd0),
|
||||||
|
.data_i (32'd0),
|
||||||
|
.strb_i (4'd0),
|
||||||
|
.ready_o (),
|
||||||
|
.data_o (),
|
||||||
|
.valid_o (),
|
||||||
|
.access_err_o (),
|
||||||
|
.model_addr_i (model_addr_o),
|
||||||
|
.model_data_o (model_store_data),
|
||||||
|
.scratch_clear_i (scratch_clear),
|
||||||
|
.scratch_read_addr_i (scratch_read_addr),
|
||||||
|
.scratch_read_data_o (scratch_read_data),
|
||||||
|
.scratch_write_valid_i(scratch_write_valid),
|
||||||
|
.scratch_write_addr_i (scratch_write_addr),
|
||||||
|
.scratch_write_data_i (scratch_write_data),
|
||||||
|
.scratch_write_strb_i (scratch_write_strb),
|
||||||
|
.scratch_access_err_o (scratch_access_err)
|
||||||
|
);
|
||||||
|
|
||||||
|
apu_kws_engine dut (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i (rst_n_i),
|
||||||
|
.soft_reset_i (1'b0),
|
||||||
|
.resource_reset_i (1'b0),
|
||||||
|
.counter_clear_i (counter_clear),
|
||||||
|
.xrun_clear_i (1'b0),
|
||||||
|
.abort_i (1'b0),
|
||||||
|
.quiesce_i (1'b0),
|
||||||
|
.disable_i (1'b0),
|
||||||
|
.flush_busy_i (1'b0),
|
||||||
|
.enable_i (1'b1),
|
||||||
|
.memory_window_i (1'b1),
|
||||||
|
.clear_history_i (clear_history),
|
||||||
|
.model_valid_i (1'b1),
|
||||||
|
.model_lock_i (1'b1),
|
||||||
|
.sequencer_timeout_i (32'd0),
|
||||||
|
.memory_start_i (memory_start),
|
||||||
|
.memory_start_ready_o (memory_start_ready_o),
|
||||||
|
.memory_address_i (MemoryBase),
|
||||||
|
.memory_dma_request_valid_o (memory_dma_request_valid_o),
|
||||||
|
.memory_dma_request_ready_i (memory_dma_request_ready),
|
||||||
|
.memory_dma_request_address_o(memory_dma_request_address_o),
|
||||||
|
.memory_dma_request_bytes_o (memory_dma_request_bytes_o),
|
||||||
|
.memory_dma_data_i (memory_dma_data),
|
||||||
|
.memory_dma_keep_i (memory_dma_keep),
|
||||||
|
.memory_dma_last_i (memory_dma_last),
|
||||||
|
.memory_dma_valid_i (memory_dma_valid),
|
||||||
|
.memory_dma_ready_o (memory_dma_ready_o),
|
||||||
|
.memory_dma_done_i (memory_dma_done),
|
||||||
|
.memory_dma_error_i (1'b0),
|
||||||
|
.memory_dma_error_code_i (6'd0),
|
||||||
|
.memory_dma_error_stage_i (4'd0),
|
||||||
|
.memory_dma_error_resp_i (2'd0),
|
||||||
|
.memory_dma_error_address_i (32'd0),
|
||||||
|
.memory_done_o (memory_done_o),
|
||||||
|
.memory_error_o (memory_error_o),
|
||||||
|
.memory_error_code_o (memory_error_code_o),
|
||||||
|
.memory_error_stage_o (memory_error_stage_o),
|
||||||
|
.memory_error_resp_o (memory_error_resp_o),
|
||||||
|
.memory_error_address_o (memory_error_address_o),
|
||||||
|
.memory_error_detail_o (memory_error_detail_o),
|
||||||
|
.memory_input_used_o (memory_input_used_o),
|
||||||
|
.kws_config_i (KwsConfig),
|
||||||
|
.input_config_i (InputConfig),
|
||||||
|
.model_addr_o (model_addr_o),
|
||||||
|
.model_data_i (model_store_data),
|
||||||
|
.scratch_clear_o (scratch_clear),
|
||||||
|
.scratch_read_addr_o (scratch_read_addr),
|
||||||
|
.scratch_read_data_i (scratch_read_data),
|
||||||
|
.scratch_write_valid_o (scratch_write_valid),
|
||||||
|
.scratch_write_addr_o (scratch_write_addr),
|
||||||
|
.scratch_write_data_o (scratch_write_data),
|
||||||
|
.scratch_write_strb_o (scratch_write_strb),
|
||||||
|
.scratch_access_err_i (scratch_access_err),
|
||||||
|
.stream_i (stream_i),
|
||||||
|
.rx_ready_o (rx_ready_o),
|
||||||
|
.status_o (status_o),
|
||||||
|
.result_o (result_o),
|
||||||
|
.timestamp_lo_o (timestamp_lo_o),
|
||||||
|
.timestamp_hi_o (timestamp_hi_o),
|
||||||
|
.frame_count_o (frame_count_o),
|
||||||
|
.inference_count_o (inference_count_o),
|
||||||
|
.hit_count_o (hit_count_o),
|
||||||
|
.overrun_count_o (overrun_count_o),
|
||||||
|
.model_status_o (model_status_o),
|
||||||
|
.model_actual_crc_o (model_actual_crc_o),
|
||||||
|
.irq_set_o (irq_set_o),
|
||||||
|
.fault_valid_o (fault_valid_o),
|
||||||
|
.fault_code_o (fault_code_o),
|
||||||
|
.fault_stage_o (fault_stage_o),
|
||||||
|
.fault_resp_o (fault_resp_o),
|
||||||
|
.fault_index_o (fault_index_o),
|
||||||
|
.fault_addr_o (fault_addr_o),
|
||||||
|
.fault_detail_o (fault_detail_o),
|
||||||
|
.model_valid_o (model_valid_o),
|
||||||
|
.model_lock_o (model_lock_o),
|
||||||
|
.busy_o (busy_o),
|
||||||
|
.idle_o (idle_o)
|
||||||
|
);
|
||||||
|
|
||||||
|
task automatic run_window(input int unsigned window_index, input string pcm_dir,
|
||||||
|
input int unsigned max_window_cycles);
|
||||||
|
string pcm_path;
|
||||||
|
int unsigned word_index;
|
||||||
|
int unsigned elapsed;
|
||||||
|
begin
|
||||||
|
pcm_path = $sformatf("%s/pcm_%06d.hex", pcm_dir, window_index);
|
||||||
|
$readmemh(pcm_path, pcm_mem);
|
||||||
|
@(negedge clk_i);
|
||||||
|
clear_history = 1'b1;
|
||||||
|
counter_clear = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
clear_history = 1'b0;
|
||||||
|
counter_clear = 1'b0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
if (!memory_start_ready_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d was not accepted at the idle boundary", window_index);
|
||||||
|
end
|
||||||
|
memory_start = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_start = 1'b0;
|
||||||
|
elapsed = 0;
|
||||||
|
while (!memory_dma_request_valid_o) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
elapsed++;
|
||||||
|
if (elapsed > 1000)
|
||||||
|
$fatal(1, "KWS memory window %0d never raised its DMA request", window_index);
|
||||||
|
end
|
||||||
|
if ((memory_dma_request_address_o != MemoryBase) ||
|
||||||
|
(memory_dma_request_bytes_o != 32'd32000)) begin
|
||||||
|
$fatal(1, "KWS memory window %0d DMA request mismatch", window_index);
|
||||||
|
end
|
||||||
|
memory_dma_request_ready = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_dma_request_ready = 1'b0;
|
||||||
|
for (word_index = 0; word_index < PcmWords; word_index++) begin
|
||||||
|
memory_dma_data = pcm_mem[word_index];
|
||||||
|
memory_dma_keep = 4'hf;
|
||||||
|
memory_dma_last = word_index == PcmWords - 1;
|
||||||
|
memory_dma_valid = 1'b1;
|
||||||
|
do @(negedge clk_i); while (!memory_dma_ready_o);
|
||||||
|
end
|
||||||
|
memory_dma_valid = 1'b0;
|
||||||
|
memory_dma_last = 1'b0;
|
||||||
|
memory_dma_done = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
memory_dma_done = 1'b0;
|
||||||
|
elapsed = 0;
|
||||||
|
while (!memory_done_o && (elapsed < max_window_cycles)) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
elapsed++;
|
||||||
|
end
|
||||||
|
if (!memory_done_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d did not complete within %0d cycles", window_index,
|
||||||
|
max_window_cycles);
|
||||||
|
end
|
||||||
|
if (memory_error_o) begin
|
||||||
|
$fatal(1, "KWS memory window %0d error code=%0d stage=%0d address=%08x detail=%08x",
|
||||||
|
window_index, memory_error_code_o, memory_error_stage_o, memory_error_address_o,
|
||||||
|
memory_error_detail_o);
|
||||||
|
end
|
||||||
|
if (memory_input_used_o != 32'd32000) begin
|
||||||
|
$fatal(1, "KWS memory window %0d consumed %0d input bytes", window_index,
|
||||||
|
memory_input_used_o);
|
||||||
|
end
|
||||||
|
if ((frame_count_o != 32'd49) || (inference_count_o != 32'd1) || !status_o[7] ||
|
||||||
|
fault_valid_o) begin
|
||||||
|
$fatal(1,
|
||||||
|
"KWS memory window %0d frames=%0d inferences=%0d status=%08x fault=%0b/%0d/%0d/%08x",
|
||||||
|
window_index, frame_count_o, inference_count_o, status_o, fault_valid_o,
|
||||||
|
fault_code_o, fault_stage_o, fault_detail_o);
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
logic [7:0] dump_byte;
|
||||||
|
$write("MFCC_HEX %0d ", window_index);
|
||||||
|
for (int feature = 0; feature < 490; feature++) begin
|
||||||
|
dump_byte = u_kws_sram_client.mem[
|
||||||
|
8192+((`RETROSOC_APU_KWS__SCRATCH_MFCC_BASE+feature)/4)
|
||||||
|
][8*((`RETROSOC_APU_KWS__SCRATCH_MFCC_BASE+feature)%4)+:8];
|
||||||
|
$write("%02x", dump_byte);
|
||||||
|
end
|
||||||
|
$display("");
|
||||||
|
$display("PEAK %0d %0d", window_index, dut.s_window_peak_q);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
string apum_hex;
|
||||||
|
string pcm_dir;
|
||||||
|
int unsigned first_index;
|
||||||
|
int unsigned window_count;
|
||||||
|
int unsigned max_window_cycles;
|
||||||
|
|
||||||
|
if (!$value$plusargs("APUM_HEX=%s", apum_hex)) $fatal(1, "missing +APUM_HEX=<path>");
|
||||||
|
if (!$value$plusargs("PCM_DIR=%s", pcm_dir)) $fatal(1, "missing +PCM_DIR=<path>");
|
||||||
|
if (!$value$plusargs("WINDOW_COUNT=%d", window_count)) begin
|
||||||
|
$fatal(1, "missing +WINDOW_COUNT=<n>");
|
||||||
|
end
|
||||||
|
if (!$value$plusargs("FIRST_INDEX=%d", first_index)) first_index = 0;
|
||||||
|
if (!$value$plusargs("MAX_WINDOW_CYCLES=%d", max_window_cycles)) begin
|
||||||
|
max_window_cycles = 4000000;
|
||||||
|
end
|
||||||
|
if (window_count == 0) $fatal(1, "WINDOW_COUNT must be positive");
|
||||||
|
|
||||||
|
repeat (4) @(negedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
if (!model_valid_o || !model_lock_o) $fatal(1, "KWS model admission was not observed");
|
||||||
|
$readmemh(apum_hex, u_kws_sram_client.mem, 0, ModelWords - 1);
|
||||||
|
|
||||||
|
for (int unsigned window = first_index; window < first_index + window_count; window++) begin
|
||||||
|
run_window(window, pcm_dir, max_window_cycles);
|
||||||
|
end
|
||||||
|
$display("APU_P7_FRONTEND_DUMP_COMPLETE windows=%0d first=%0d", window_count, first_index);
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,807 @@
|
|||||||
|
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
// 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 the Mulan PSL v2 at:
|
||||||
|
// http://license.coscl.org.cn/MulanPSL2
|
||||||
|
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF
|
||||||
|
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
// See the Mulan PSL v2 for more details.
|
||||||
|
|
||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// APU-P8 Gateway A (APU/SDIO0/USB2) contention evidence.
|
||||||
|
//
|
||||||
|
// Three client BFMs model the frozen docs/ip/apu.md burst profiles on the
|
||||||
|
// real hp_axi4_mux3 round-robin gateway: the APU private DMA (1-16-beat INCR
|
||||||
|
// descriptor/codec traffic), the SDIO0 DMA block engine (8-16-beat block
|
||||||
|
// transfers), and the USB2 DMA engine (4-8-beat transfers). All bursts are
|
||||||
|
// 4 KiB aligned so they never cross a 4 KiB boundary. A downstream model
|
||||||
|
// applies bounded random stalls (countdown-bounded, at most MaxStall cycles)
|
||||||
|
// on AW/AR/W/R/B. All randomness comes from seeded xorshift generators; the
|
||||||
|
// seed defaults to a fixed value and can be overridden with +SEED=<n>.
|
||||||
|
//
|
||||||
|
// The scoreboard proves the frozen Gateway A contract:
|
||||||
|
// (a) every granted ownership is retained until terminal B or RLAST: no
|
||||||
|
// downstream address is accepted while a read or write burst is in
|
||||||
|
// flight, R/W/B handshakes only ever reach the owning client, beat
|
||||||
|
// counts and RLAST/WLAST placement are exact, and end-to-end data tags
|
||||||
|
// are uncorrupted in both directions;
|
||||||
|
// (b) a continuously eligible source is granted within three arbitration
|
||||||
|
// decisions, i.e. at most two foreign address grants may pass while it
|
||||||
|
// waits, so no client can starve while downstream traffic completes;
|
||||||
|
// (c) all three clients run to completion with exactly their planned beat
|
||||||
|
// counts on the shared fabric identity (address-region attribution,
|
||||||
|
// cross-checked between scoreboard, BFMs, and the downstream model);
|
||||||
|
// (d) idle is declared only after all three clients, the gateway, and the
|
||||||
|
// downstream model stay quiescent for eight consecutive cycles.
|
||||||
|
|
||||||
|
// Seeded xorshift client BFM: one outstanding transaction at a time, random
|
||||||
|
// burst lengths, random 4 KiB-aligned addresses, random inter-burst gaps,
|
||||||
|
// and random R/W/B backpressure.
|
||||||
|
module gateway_client_bfm #(
|
||||||
|
parameter int unsigned ClientId = 0,
|
||||||
|
parameter int unsigned ReadBursts = 8,
|
||||||
|
parameter int unsigned WriteBursts = 8,
|
||||||
|
parameter int unsigned RdMinBeats = 1,
|
||||||
|
parameter int unsigned RdMaxBeats = 16,
|
||||||
|
parameter int unsigned WrMinBeats = 1,
|
||||||
|
parameter int unsigned WrMaxBeats = 16,
|
||||||
|
parameter logic [31:0] BaseAddr = 32'h0001_0000
|
||||||
|
) (
|
||||||
|
input logic clk_i,
|
||||||
|
input logic rst_n_i,
|
||||||
|
input logic start_i,
|
||||||
|
input logic [31:0] seed_i,
|
||||||
|
output logic done_o,
|
||||||
|
axi4_if.master axi4
|
||||||
|
);
|
||||||
|
logic [31:0] s_rand_q = 32'h1;
|
||||||
|
int unsigned planned_read_beats = 0;
|
||||||
|
int unsigned planned_write_beats = 0;
|
||||||
|
int unsigned planned_read_bursts = 0;
|
||||||
|
int unsigned planned_write_bursts = 0;
|
||||||
|
int unsigned reads_done;
|
||||||
|
int unsigned writes_done;
|
||||||
|
|
||||||
|
function automatic logic [31:0] data_pattern(input logic [31:0] addr_i,
|
||||||
|
input int unsigned beat_i);
|
||||||
|
logic [31:0] value;
|
||||||
|
begin
|
||||||
|
value = (addr_i << 5) ^ (addr_i >> 2) ^ 32'hC350_F00D;
|
||||||
|
value = value ^ (32'h0001_0101 * 32'(beat_i));
|
||||||
|
return value;
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
task automatic rand_next(output logic [31:0] value_o);
|
||||||
|
begin
|
||||||
|
s_rand_q = s_rand_q ^ (s_rand_q << 13);
|
||||||
|
s_rand_q = s_rand_q ^ (s_rand_q >> 17);
|
||||||
|
s_rand_q = s_rand_q ^ (s_rand_q << 5);
|
||||||
|
value_o = s_rand_q;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// 4 KiB block 0..7 inside the client region, offset so the burst never
|
||||||
|
// crosses a 4 KiB boundary.
|
||||||
|
function automatic logic [31:0] burst_addr(input logic [31:0] rand_i, input int unsigned beats_i);
|
||||||
|
int unsigned word_offset;
|
||||||
|
begin
|
||||||
|
word_offset = int'(rand_i[9:0]) % (1024 - beats_i + 1);
|
||||||
|
return BaseAddr + 32'(int'(rand_i[15:13]) * 4096) + 32'(word_offset * 4);
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
task automatic do_read_burst(input logic [31:0] addr_i, input int unsigned beats_i);
|
||||||
|
int unsigned beat;
|
||||||
|
logic [31:0] rand_l;
|
||||||
|
logic [31:0] expected;
|
||||||
|
begin
|
||||||
|
planned_read_beats = planned_read_beats + beats_i;
|
||||||
|
planned_read_bursts = planned_read_bursts + 1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.araddr = addr_i;
|
||||||
|
axi4.arlen = 8'(beats_i - 1);
|
||||||
|
axi4.arsize = 3'd2;
|
||||||
|
axi4.arburst = 2'b01;
|
||||||
|
axi4.arvalid = 1'b1;
|
||||||
|
@(posedge clk_i);
|
||||||
|
while (!axi4.arready) @(posedge clk_i);
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.arvalid = 1'b0;
|
||||||
|
beat = 0;
|
||||||
|
while (beat < beats_i) begin
|
||||||
|
rand_next(rand_l);
|
||||||
|
axi4.rready = ((rand_l % 5) != 32'd0);
|
||||||
|
@(posedge clk_i);
|
||||||
|
if (axi4.rvalid && axi4.rready) begin
|
||||||
|
expected = data_pattern(addr_i + 32'(beat * 4), beat);
|
||||||
|
if ((axi4.rdata !== expected) || (axi4.rresp !== 2'b00) ||
|
||||||
|
(axi4.rlast !== (beat == (beats_i - 1)))) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A client %0d read data integrity failed at beat %0d",
|
||||||
|
ClientId, beat);
|
||||||
|
end
|
||||||
|
beat = beat + 1;
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
end
|
||||||
|
axi4.rready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic do_write_burst(input logic [31:0] addr_i, input int unsigned beats_i);
|
||||||
|
int unsigned beat;
|
||||||
|
logic [31:0] rand_l;
|
||||||
|
begin
|
||||||
|
planned_write_beats = planned_write_beats + beats_i;
|
||||||
|
planned_write_bursts = planned_write_bursts + 1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.awaddr = addr_i;
|
||||||
|
axi4.awlen = 8'(beats_i - 1);
|
||||||
|
axi4.awsize = 3'd2;
|
||||||
|
axi4.awburst = 2'b01;
|
||||||
|
axi4.awvalid = 1'b1;
|
||||||
|
@(posedge clk_i);
|
||||||
|
while (!axi4.awready) @(posedge clk_i);
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.awvalid = 1'b0;
|
||||||
|
beat = 0;
|
||||||
|
while (beat < beats_i) begin
|
||||||
|
rand_next(rand_l);
|
||||||
|
if ((rand_l % 4) == 32'd0) @(negedge clk_i);
|
||||||
|
axi4.wdata = data_pattern(addr_i + 32'(beat * 4), beat);
|
||||||
|
axi4.wstrb = 4'hF;
|
||||||
|
axi4.wlast = (beat == (beats_i - 1));
|
||||||
|
axi4.wvalid = 1'b1;
|
||||||
|
@(posedge clk_i);
|
||||||
|
while (!axi4.wready) @(posedge clk_i);
|
||||||
|
beat = beat + 1;
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.wvalid = 1'b0;
|
||||||
|
axi4.wlast = 1'b0;
|
||||||
|
end
|
||||||
|
rand_next(rand_l);
|
||||||
|
repeat (rand_l % 4) begin
|
||||||
|
axi4.bready = 1'b0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
end
|
||||||
|
axi4.bready = 1'b1;
|
||||||
|
@(posedge clk_i);
|
||||||
|
while (!axi4.bvalid) @(posedge clk_i);
|
||||||
|
if (axi4.bresp !== 2'b00) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A client %0d write response error", ClientId);
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
axi4.bready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
axi4.awid = 1'b0;
|
||||||
|
axi4.awaddr = 32'd0;
|
||||||
|
axi4.awlen = 8'd0;
|
||||||
|
axi4.awsize = 3'd2;
|
||||||
|
axi4.awburst = 2'b01;
|
||||||
|
axi4.awlock = 1'b0;
|
||||||
|
axi4.awcache = 4'd0;
|
||||||
|
axi4.awprot = 3'd0;
|
||||||
|
axi4.awqos = 4'd0;
|
||||||
|
axi4.awregion = 4'd0;
|
||||||
|
axi4.awuser = 1'b0;
|
||||||
|
axi4.awvalid = 1'b0;
|
||||||
|
axi4.wdata = 32'd0;
|
||||||
|
axi4.wstrb = 4'h0;
|
||||||
|
axi4.wlast = 1'b0;
|
||||||
|
axi4.wuser = 1'b0;
|
||||||
|
axi4.wvalid = 1'b0;
|
||||||
|
axi4.bready = 1'b0;
|
||||||
|
axi4.arid = 1'b0;
|
||||||
|
axi4.araddr = 32'd0;
|
||||||
|
axi4.arlen = 8'd0;
|
||||||
|
axi4.arsize = 3'd2;
|
||||||
|
axi4.arburst = 2'b01;
|
||||||
|
axi4.arlock = 1'b0;
|
||||||
|
axi4.arcache = 4'd0;
|
||||||
|
axi4.arprot = 3'd0;
|
||||||
|
axi4.arqos = 4'd0;
|
||||||
|
axi4.arregion = 4'd0;
|
||||||
|
axi4.aruser = 1'b0;
|
||||||
|
axi4.arvalid = 1'b0;
|
||||||
|
axi4.rready = 1'b0;
|
||||||
|
done_o = 1'b0;
|
||||||
|
wait (rst_n_i === 1'b1);
|
||||||
|
wait (start_i === 1'b1);
|
||||||
|
s_rand_q = seed_i ^ (32'h9E37_79B9 * 32'(ClientId + 1));
|
||||||
|
if (s_rand_q == 32'd0) s_rand_q = 32'h1;
|
||||||
|
reads_done = 0;
|
||||||
|
writes_done = 0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
while ((reads_done < ReadBursts) || (writes_done < WriteBursts)) begin
|
||||||
|
logic [31:0] rand_l;
|
||||||
|
int unsigned beats;
|
||||||
|
rand_next(rand_l);
|
||||||
|
if ((reads_done < ReadBursts) && ((writes_done >= WriteBursts) || rand_l[0])) begin
|
||||||
|
rand_next(rand_l);
|
||||||
|
beats = RdMinBeats + (int'(rand_l[7:0]) % (RdMaxBeats - RdMinBeats + 1));
|
||||||
|
rand_next(rand_l);
|
||||||
|
do_read_burst(burst_addr(rand_l, beats), beats);
|
||||||
|
reads_done = reads_done + 1;
|
||||||
|
end else begin
|
||||||
|
rand_next(rand_l);
|
||||||
|
beats = WrMinBeats + (int'(rand_l[7:0]) % (WrMaxBeats - WrMinBeats + 1));
|
||||||
|
rand_next(rand_l);
|
||||||
|
do_write_burst(burst_addr(rand_l, beats), beats);
|
||||||
|
writes_done = writes_done + 1;
|
||||||
|
end
|
||||||
|
rand_next(rand_l);
|
||||||
|
repeat (rand_l % 4) @(negedge clk_i);
|
||||||
|
end
|
||||||
|
done_o = 1'b1;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
// Shared-fabric downstream model: one transaction per direction, exact
|
||||||
|
// beat/RLAST/WLAST accounting, per-client traffic counters attributed by
|
||||||
|
// address region, and countdown-bounded random stalls on every channel.
|
||||||
|
module gateway_downstream_model #(
|
||||||
|
parameter int unsigned MaxStall = 5
|
||||||
|
) (
|
||||||
|
input logic clk_i,
|
||||||
|
input logic rst_n_i,
|
||||||
|
input logic [31:0] seed_i,
|
||||||
|
output logic idle_o,
|
||||||
|
axi4_if.slave axi4
|
||||||
|
);
|
||||||
|
localparam logic [1:0] WrIdle = 2'd0;
|
||||||
|
localparam logic [1:0] WrData = 2'd1;
|
||||||
|
localparam logic [1:0] WrResp = 2'd2;
|
||||||
|
localparam logic RdIdle = 1'd0;
|
||||||
|
localparam logic RdData = 1'd1;
|
||||||
|
|
||||||
|
logic [ 1:0] s_wr_state_q;
|
||||||
|
logic s_rd_state_q;
|
||||||
|
logic [31:0] s_wr_addr_q;
|
||||||
|
logic [31:0] s_rd_addr_q;
|
||||||
|
logic [ 7:0] s_wr_len_q;
|
||||||
|
logic [ 7:0] s_rd_len_q;
|
||||||
|
logic [ 8:0] s_wr_beat_q;
|
||||||
|
logic [ 8:0] s_rd_beat_q;
|
||||||
|
logic [ 3:0] s_aw_delay_q;
|
||||||
|
logic [ 3:0] s_w_delay_q;
|
||||||
|
logic [ 3:0] s_b_delay_q;
|
||||||
|
logic [ 3:0] s_ar_delay_q;
|
||||||
|
logic [ 3:0] s_r_delay_q;
|
||||||
|
logic [31:0] s_rand_wr_q = 32'h1;
|
||||||
|
logic [31:0] s_rand_rd_q = 32'h1;
|
||||||
|
logic [31:0] s_rand_t;
|
||||||
|
int unsigned read_beats [0:2];
|
||||||
|
int unsigned write_beats [0:2];
|
||||||
|
|
||||||
|
function automatic logic [31:0] data_pattern(input logic [31:0] addr_i,
|
||||||
|
input int unsigned beat_i);
|
||||||
|
logic [31:0] value;
|
||||||
|
begin
|
||||||
|
value = (addr_i << 5) ^ (addr_i >> 2) ^ 32'hC350_F00D;
|
||||||
|
value = value ^ (32'h0001_0101 * 32'(beat_i));
|
||||||
|
return value;
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
assign idle_o = (s_wr_state_q == WrIdle) && (s_rd_state_q == RdIdle);
|
||||||
|
assign axi4.awready = (s_wr_state_q == WrIdle) && (s_aw_delay_q == 4'd0);
|
||||||
|
assign axi4.wready = (s_wr_state_q == WrData) && (s_w_delay_q == 4'd0);
|
||||||
|
assign axi4.bid = 1'b0;
|
||||||
|
assign axi4.bresp = 2'b00;
|
||||||
|
assign axi4.buser = 1'b0;
|
||||||
|
assign axi4.bvalid = (s_wr_state_q == WrResp) && (s_b_delay_q == 4'd0);
|
||||||
|
assign axi4.arready = (s_rd_state_q == RdIdle) && (s_ar_delay_q == 4'd0);
|
||||||
|
assign axi4.rid = 1'b0;
|
||||||
|
assign axi4.rdata = data_pattern(s_rd_addr_q + {21'd0, s_rd_beat_q, 2'b00}, int'(s_rd_beat_q));
|
||||||
|
assign axi4.rresp = 2'b00;
|
||||||
|
assign axi4.rlast = (s_rd_beat_q == {1'b0, s_rd_len_q});
|
||||||
|
assign axi4.ruser = 1'b0;
|
||||||
|
assign axi4.rvalid = (s_rd_state_q == RdData) && (s_r_delay_q == 4'd0);
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
s_wr_state_q <= WrIdle;
|
||||||
|
s_wr_addr_q <= 32'd0;
|
||||||
|
s_wr_len_q <= 8'd0;
|
||||||
|
s_wr_beat_q <= 9'd0;
|
||||||
|
s_aw_delay_q <= 4'd0;
|
||||||
|
s_w_delay_q <= 4'd0;
|
||||||
|
s_b_delay_q <= 4'd0;
|
||||||
|
write_beats[0] <= 0;
|
||||||
|
write_beats[1] <= 0;
|
||||||
|
write_beats[2] <= 0;
|
||||||
|
end else begin
|
||||||
|
case (s_wr_state_q)
|
||||||
|
WrIdle: begin
|
||||||
|
if (axi4.awvalid && (s_aw_delay_q != 4'd0)) s_aw_delay_q <= s_aw_delay_q - 4'd1;
|
||||||
|
if (axi4.awvalid && axi4.awready) begin
|
||||||
|
if ((axi4.awaddr[31:20] != 12'h000) || (axi4.awaddr[19:16] < 4'd1) ||
|
||||||
|
(axi4.awaddr[19:16] > 4'd3)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A delivered an unattributable write address 0x%08h",
|
||||||
|
axi4.awaddr);
|
||||||
|
end
|
||||||
|
if ((axi4.awburst != 2'b01) || (axi4.awsize != 3'd2)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A corrupted a write address channel");
|
||||||
|
end
|
||||||
|
s_wr_addr_q <= axi4.awaddr;
|
||||||
|
s_wr_len_q <= axi4.awlen;
|
||||||
|
s_wr_beat_q <= 9'd0;
|
||||||
|
s_wr_state_q <= WrData;
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 13);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q >> 17);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 5);
|
||||||
|
s_w_delay_q <= 4'(s_rand_wr_q % 32'(MaxStall + 1));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
WrData: begin
|
||||||
|
if (axi4.wvalid && axi4.wready) begin
|
||||||
|
if (axi4.wdata !== data_pattern(
|
||||||
|
s_wr_addr_q + {21'd0, s_wr_beat_q, 2'b00}, int'(s_wr_beat_q)
|
||||||
|
)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A corrupted write data beat %0d", s_wr_beat_q);
|
||||||
|
end
|
||||||
|
if (axi4.wstrb !== 4'hF) $fatal(1, "APU-P8 Gateway A corrupted write strobes");
|
||||||
|
if (axi4.wlast !== (s_wr_beat_q == {1'b0, s_wr_len_q})) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A corrupted WLAST placement");
|
||||||
|
end
|
||||||
|
write_beats[int'(s_wr_addr_q[19:16])-1] <= write_beats[int'(s_wr_addr_q[19:16])-1] + 1;
|
||||||
|
s_wr_beat_q <= s_wr_beat_q + 9'd1;
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 13);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q >> 17);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 5);
|
||||||
|
s_w_delay_q <= 4'(s_rand_wr_q % 32'(MaxStall + 1));
|
||||||
|
if (axi4.wlast) begin
|
||||||
|
s_wr_state_q <= WrResp;
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 13);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q >> 17);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 5);
|
||||||
|
s_b_delay_q <= 4'(s_rand_wr_q % 32'(MaxStall + 1));
|
||||||
|
end
|
||||||
|
end else if (axi4.wvalid && (s_w_delay_q != 4'd0)) begin
|
||||||
|
s_w_delay_q <= s_w_delay_q - 4'd1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
WrResp: begin
|
||||||
|
if (s_b_delay_q != 4'd0) s_b_delay_q <= s_b_delay_q - 4'd1;
|
||||||
|
if (axi4.bvalid && axi4.bready) begin
|
||||||
|
s_wr_state_q <= WrIdle;
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 13);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q >> 17);
|
||||||
|
s_rand_wr_q = s_rand_wr_q ^ (s_rand_wr_q << 5);
|
||||||
|
s_aw_delay_q <= 4'(s_rand_wr_q % 32'(MaxStall + 1));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
default: s_wr_state_q <= WrIdle;
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
s_rd_state_q <= RdIdle;
|
||||||
|
s_rd_addr_q <= 32'd0;
|
||||||
|
s_rd_len_q <= 8'd0;
|
||||||
|
s_rd_beat_q <= 9'd0;
|
||||||
|
s_ar_delay_q <= 4'd0;
|
||||||
|
s_r_delay_q <= 4'd0;
|
||||||
|
read_beats[0] <= 0;
|
||||||
|
read_beats[1] <= 0;
|
||||||
|
read_beats[2] <= 0;
|
||||||
|
end else begin
|
||||||
|
case (s_rd_state_q)
|
||||||
|
RdIdle: begin
|
||||||
|
if (axi4.arvalid && (s_ar_delay_q != 4'd0)) s_ar_delay_q <= s_ar_delay_q - 4'd1;
|
||||||
|
if (axi4.arvalid && axi4.arready) begin
|
||||||
|
if ((axi4.araddr[31:20] != 12'h000) || (axi4.araddr[19:16] < 4'd1) ||
|
||||||
|
(axi4.araddr[19:16] > 4'd3)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A delivered an unattributable read address 0x%08h",
|
||||||
|
axi4.araddr);
|
||||||
|
end
|
||||||
|
if ((axi4.arburst != 2'b01) || (axi4.arsize != 3'd2)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A corrupted a read address channel");
|
||||||
|
end
|
||||||
|
s_rd_addr_q <= axi4.araddr;
|
||||||
|
s_rd_len_q <= axi4.arlen;
|
||||||
|
s_rd_beat_q <= 9'd0;
|
||||||
|
s_rd_state_q <= RdData;
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 13);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q >> 17);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 5);
|
||||||
|
s_r_delay_q <= 4'(s_rand_rd_q % 32'(MaxStall + 1));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RdData: begin
|
||||||
|
if (s_r_delay_q != 4'd0) s_r_delay_q <= s_r_delay_q - 4'd1;
|
||||||
|
if (axi4.rvalid && axi4.rready) begin
|
||||||
|
read_beats[int'(s_rd_addr_q[19:16])-1] <= read_beats[int'(s_rd_addr_q[19:16])-1] + 1;
|
||||||
|
s_rd_beat_q <= s_rd_beat_q + 9'd1;
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 13);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q >> 17);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 5);
|
||||||
|
s_r_delay_q <= 4'(s_rand_rd_q % 32'(MaxStall + 1));
|
||||||
|
if (axi4.rlast) begin
|
||||||
|
s_rd_state_q <= RdIdle;
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 13);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q >> 17);
|
||||||
|
s_rand_rd_q = s_rand_rd_q ^ (s_rand_rd_q << 5);
|
||||||
|
s_ar_delay_q <= 4'(s_rand_rd_q % 32'(MaxStall + 1));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
default: s_rd_state_q <= RdIdle;
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
wait (rst_n_i === 1'b1);
|
||||||
|
s_rand_wr_q = seed_i ^ 32'hD15E_A000;
|
||||||
|
s_rand_rd_q = seed_i ^ 32'hD15E_B000;
|
||||||
|
if (s_rand_wr_q == 32'd0) s_rand_wr_q = 32'h1;
|
||||||
|
if (s_rand_rd_q == 32'd0) s_rand_rd_q = 32'h1;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
module apu_p8_gateway_contention_tb;
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic start_i = 1'b0;
|
||||||
|
logic [31:0] seed = 32'd20260916;
|
||||||
|
logic [ 7:0] epoch_i = 8'd0;
|
||||||
|
logic s_apu_done;
|
||||||
|
logic s_sdio0_done;
|
||||||
|
logic s_usb2_done;
|
||||||
|
logic s_downstream_idle;
|
||||||
|
logic s_quiescent;
|
||||||
|
logic s_rd_active_q;
|
||||||
|
logic [ 1:0] s_rd_owner_q;
|
||||||
|
logic [ 7:0] s_rd_len_q;
|
||||||
|
logic [ 8:0] s_rd_beat_q;
|
||||||
|
logic s_wr_active_q;
|
||||||
|
logic s_wr_data_done_q;
|
||||||
|
logic [ 1:0] s_wr_owner_q;
|
||||||
|
logic [ 7:0] s_wr_len_q;
|
||||||
|
logic [ 8:0] s_wr_beat_q;
|
||||||
|
logic [ 2:0] s_eligible;
|
||||||
|
logic [ 2:0] s_rvalids;
|
||||||
|
logic [ 2:0] s_wreadys;
|
||||||
|
logic [ 2:0] s_bvalids;
|
||||||
|
logic s_accept;
|
||||||
|
logic [ 1:0] s_grant;
|
||||||
|
int unsigned s_wait_q [0:2];
|
||||||
|
int unsigned s_max_wait_q;
|
||||||
|
int unsigned s_sb_read_beats [0:2];
|
||||||
|
int unsigned s_sb_write_beats [0:2];
|
||||||
|
int unsigned s_sb_read_bursts [0:2];
|
||||||
|
int unsigned s_sb_write_bursts [0:2];
|
||||||
|
int unsigned total_beats;
|
||||||
|
int quiet;
|
||||||
|
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) apu (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) sdio0 (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) usb2 (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) axi4 (
|
||||||
|
clk_i,
|
||||||
|
rst_n_i
|
||||||
|
);
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
gateway_client_bfm #(
|
||||||
|
.ClientId (0),
|
||||||
|
.ReadBursts (40),
|
||||||
|
.WriteBursts(40),
|
||||||
|
.RdMinBeats (1),
|
||||||
|
.RdMaxBeats (16),
|
||||||
|
.WrMinBeats (1),
|
||||||
|
.WrMaxBeats (16),
|
||||||
|
.BaseAddr (32'h0001_0000)
|
||||||
|
) u_apu_bfm (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.start_i,
|
||||||
|
.seed_i(seed ^ 32'h0000_0A90),
|
||||||
|
.done_o(s_apu_done),
|
||||||
|
.axi4 (apu)
|
||||||
|
);
|
||||||
|
gateway_client_bfm #(
|
||||||
|
.ClientId (1),
|
||||||
|
.ReadBursts (24),
|
||||||
|
.WriteBursts(24),
|
||||||
|
.RdMinBeats (8),
|
||||||
|
.RdMaxBeats (16),
|
||||||
|
.WrMinBeats (8),
|
||||||
|
.WrMaxBeats (16),
|
||||||
|
.BaseAddr (32'h0002_0000)
|
||||||
|
) u_sdio0_bfm (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.start_i,
|
||||||
|
.seed_i(seed ^ 32'h0000_5D10),
|
||||||
|
.done_o(s_sdio0_done),
|
||||||
|
.axi4 (sdio0)
|
||||||
|
);
|
||||||
|
gateway_client_bfm #(
|
||||||
|
.ClientId (2),
|
||||||
|
.ReadBursts (32),
|
||||||
|
.WriteBursts(32),
|
||||||
|
.RdMinBeats (4),
|
||||||
|
.RdMaxBeats (8),
|
||||||
|
.WrMinBeats (4),
|
||||||
|
.WrMaxBeats (8),
|
||||||
|
.BaseAddr (32'h0003_0000)
|
||||||
|
) u_usb2_bfm (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.start_i,
|
||||||
|
.seed_i(seed ^ 32'h0000_0B22),
|
||||||
|
.done_o(s_usb2_done),
|
||||||
|
.axi4 (usb2)
|
||||||
|
);
|
||||||
|
|
||||||
|
gateway_downstream_model #(
|
||||||
|
.MaxStall(5)
|
||||||
|
) u_downstream (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.seed_i(seed ^ 32'h0000_D00D),
|
||||||
|
.idle_o(s_downstream_idle),
|
||||||
|
.axi4 (axi4)
|
||||||
|
);
|
||||||
|
|
||||||
|
hp_axi4_mux3 #(
|
||||||
|
.RoundRobin (1'b1),
|
||||||
|
.Client0EpochAware(1'b1)
|
||||||
|
) u_dut (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.epoch_i,
|
||||||
|
.icache(apu),
|
||||||
|
.dcache(sdio0),
|
||||||
|
.mmio (usb2),
|
||||||
|
.axi4 (axi4)
|
||||||
|
);
|
||||||
|
|
||||||
|
assign s_eligible = {
|
||||||
|
usb2.arvalid || usb2.awvalid, sdio0.arvalid || sdio0.awvalid, apu.arvalid || apu.awvalid
|
||||||
|
};
|
||||||
|
assign s_rvalids = {usb2.rvalid, sdio0.rvalid, apu.rvalid};
|
||||||
|
assign s_wreadys = {usb2.wready, sdio0.wready, apu.wready};
|
||||||
|
assign s_bvalids = {usb2.bvalid, sdio0.bvalid, apu.bvalid};
|
||||||
|
assign s_accept = (axi4.arvalid && axi4.arready) || (axi4.awvalid && axi4.awready);
|
||||||
|
assign s_grant = (axi4.arvalid && axi4.arready) ? (axi4.araddr[17:16] - 2'd1)
|
||||||
|
: (axi4.awaddr[17:16] - 2'd1);
|
||||||
|
assign s_quiescent = s_apu_done && s_sdio0_done && s_usb2_done && !apu.arvalid &&
|
||||||
|
!apu.awvalid && !apu.wvalid && !sdio0.arvalid && !sdio0.awvalid && !sdio0.wvalid &&
|
||||||
|
!usb2.arvalid && !usb2.awvalid && !usb2.wvalid && !axi4.arvalid && !axi4.awvalid &&
|
||||||
|
!axi4.wvalid && !axi4.rvalid && !axi4.bvalid && s_downstream_idle &&
|
||||||
|
(u_dut.s_state_q == 3'd0) && !s_rd_active_q && !s_wr_active_q;
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
s_rd_active_q <= 1'b0;
|
||||||
|
s_rd_owner_q <= 2'd0;
|
||||||
|
s_rd_len_q <= 8'd0;
|
||||||
|
s_rd_beat_q <= 9'd0;
|
||||||
|
s_wr_active_q <= 1'b0;
|
||||||
|
s_wr_data_done_q <= 1'b0;
|
||||||
|
s_wr_owner_q <= 2'd0;
|
||||||
|
s_wr_len_q <= 8'd0;
|
||||||
|
s_wr_beat_q <= 9'd0;
|
||||||
|
s_wait_q[0] <= 0;
|
||||||
|
s_wait_q[1] <= 0;
|
||||||
|
s_wait_q[2] <= 0;
|
||||||
|
s_max_wait_q <= 0;
|
||||||
|
s_sb_read_beats[0] <= 0;
|
||||||
|
s_sb_read_beats[1] <= 0;
|
||||||
|
s_sb_read_beats[2] <= 0;
|
||||||
|
s_sb_write_beats[0] <= 0;
|
||||||
|
s_sb_write_beats[1] <= 0;
|
||||||
|
s_sb_write_beats[2] <= 0;
|
||||||
|
s_sb_read_bursts[0] <= 0;
|
||||||
|
s_sb_read_bursts[1] <= 0;
|
||||||
|
s_sb_read_bursts[2] <= 0;
|
||||||
|
s_sb_write_bursts[0] <= 0;
|
||||||
|
s_sb_write_bursts[1] <= 0;
|
||||||
|
s_sb_write_bursts[2] <= 0;
|
||||||
|
end else begin
|
||||||
|
// (a) Ownership retention: no new address while a burst is in flight.
|
||||||
|
if (axi4.arvalid && axi4.arready) begin
|
||||||
|
if (s_rd_active_q || s_wr_active_q) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A accepted a read address before terminal B/RLAST");
|
||||||
|
end
|
||||||
|
s_rd_active_q <= 1'b1;
|
||||||
|
s_rd_owner_q <= axi4.araddr[17:16] - 2'd1;
|
||||||
|
s_rd_len_q <= axi4.arlen;
|
||||||
|
s_rd_beat_q <= 9'd0;
|
||||||
|
s_sb_read_bursts[axi4.araddr[17:16]-2'd1] <= s_sb_read_bursts[axi4.araddr[17:16]-2'd1] + 1;
|
||||||
|
end
|
||||||
|
if (axi4.awvalid && axi4.awready) begin
|
||||||
|
if (s_rd_active_q || s_wr_active_q) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A accepted a write address before terminal B/RLAST");
|
||||||
|
end
|
||||||
|
s_wr_active_q <= 1'b1;
|
||||||
|
s_wr_data_done_q <= 1'b0;
|
||||||
|
s_wr_owner_q <= axi4.awaddr[17:16] - 2'd1;
|
||||||
|
s_wr_len_q <= axi4.awlen;
|
||||||
|
s_wr_beat_q <= 9'd0;
|
||||||
|
s_sb_write_bursts[axi4.awaddr[17:16]-2'd1] <=
|
||||||
|
s_sb_write_bursts[axi4.awaddr[17:16]-2'd1] + 1;
|
||||||
|
end
|
||||||
|
if (axi4.wvalid && axi4.wready) begin
|
||||||
|
if (!s_wr_active_q || s_wr_data_done_q) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A delivered an orphaned write beat");
|
||||||
|
end
|
||||||
|
if (axi4.wlast !== (s_wr_beat_q == {1'b0, s_wr_len_q})) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A moved the write burst terminal");
|
||||||
|
end
|
||||||
|
s_wr_beat_q <= s_wr_beat_q + 9'd1;
|
||||||
|
s_sb_write_beats[s_wr_owner_q] <= s_sb_write_beats[s_wr_owner_q] + 1;
|
||||||
|
if (axi4.wlast) s_wr_data_done_q <= 1'b1;
|
||||||
|
end
|
||||||
|
if (axi4.bvalid && axi4.bready) begin
|
||||||
|
if (!s_wr_active_q || !s_wr_data_done_q) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A delivered B before the write data terminal");
|
||||||
|
end
|
||||||
|
s_wr_active_q <= 1'b0;
|
||||||
|
s_wr_data_done_q <= 1'b0;
|
||||||
|
end
|
||||||
|
if (axi4.rvalid && axi4.rready) begin
|
||||||
|
if (!s_rd_active_q) $fatal(1, "APU-P8 Gateway A delivered an orphaned read beat");
|
||||||
|
if (axi4.rlast !== (s_rd_beat_q == {1'b0, s_rd_len_q})) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A moved the read burst terminal");
|
||||||
|
end
|
||||||
|
if (axi4.rresp !== 2'b00) $fatal(1, "APU-P8 Gateway A read error response");
|
||||||
|
s_rd_beat_q <= s_rd_beat_q + 9'd1;
|
||||||
|
s_sb_read_beats[s_rd_owner_q] <= s_sb_read_beats[s_rd_owner_q] + 1;
|
||||||
|
if (axi4.rlast) s_rd_active_q <= 1'b0;
|
||||||
|
end
|
||||||
|
|
||||||
|
// (a) Client-side routing: R/W/B handshakes only ever reach the owner.
|
||||||
|
if (s_rd_active_q) begin
|
||||||
|
if ((s_rvalids != 3'b000) && (s_rvalids != (3'b001 << s_rd_owner_q))) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A routed read data to a non-owner client");
|
||||||
|
end
|
||||||
|
end else if (s_rvalids != 3'b000) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A asserted read data with no active read burst");
|
||||||
|
end
|
||||||
|
if (s_wr_active_q && !s_wr_data_done_q) begin
|
||||||
|
if ((s_wreadys != 3'b000) && (s_wreadys != (3'b001 << s_wr_owner_q))) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A routed write ready to a non-owner client");
|
||||||
|
end
|
||||||
|
end else if (s_wreadys != 3'b000) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A asserted write ready with no active write data");
|
||||||
|
end
|
||||||
|
if (s_wr_active_q) begin
|
||||||
|
if ((s_bvalids != 3'b000) && (s_bvalids != (3'b001 << s_wr_owner_q))) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A routed a write response to a non-owner client");
|
||||||
|
end
|
||||||
|
end else if (s_bvalids != 3'b000) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A asserted a write response with no active write burst");
|
||||||
|
end
|
||||||
|
|
||||||
|
// (b) No starvation: a continuously eligible source loses at most two
|
||||||
|
// arbitration decisions before it is granted (bound of three).
|
||||||
|
if (s_accept) begin
|
||||||
|
for (int client = 0; client < 3; client++) begin
|
||||||
|
if (client == int'(s_grant)) begin
|
||||||
|
s_wait_q[client] <= 0;
|
||||||
|
end else if (s_eligible[client]) begin
|
||||||
|
if (s_wait_q[client] >= 2) begin
|
||||||
|
$fatal(1,
|
||||||
|
"APU-P8 Gateway A starvation: client %0d lost three consecutive arbitrations",
|
||||||
|
client);
|
||||||
|
end
|
||||||
|
s_wait_q[client] <= s_wait_q[client] + 1;
|
||||||
|
if ((s_wait_q[client] + 1) > s_max_wait_q) s_max_wait_q <= s_wait_q[client] + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
void'($value$plusargs("SEED=%d", seed));
|
||||||
|
if (seed == 32'd0) seed = 32'd20260916;
|
||||||
|
repeat (4) @(posedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
repeat (2) @(negedge clk_i);
|
||||||
|
start_i = 1'b1;
|
||||||
|
wait (s_apu_done && s_sdio0_done && s_usb2_done);
|
||||||
|
start_i = 1'b0;
|
||||||
|
|
||||||
|
// (d) Idle is declared only after all three clients, the gateway, and
|
||||||
|
// the downstream model stay quiescent for eight consecutive cycles.
|
||||||
|
quiet = 0;
|
||||||
|
while (quiet < 8) begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
if (s_quiescent) quiet = quiet + 1;
|
||||||
|
else quiet = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
// (c) Completion with expected byte counts, cross-checked between the
|
||||||
|
// scoreboard, the BFMs, and the downstream model.
|
||||||
|
if ((s_sb_read_beats[0] != u_apu_bfm.planned_read_beats) ||
|
||||||
|
(s_sb_read_beats[1] != u_sdio0_bfm.planned_read_beats) ||
|
||||||
|
(s_sb_read_beats[2] != u_usb2_bfm.planned_read_beats)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A read beat attribution mismatch");
|
||||||
|
end
|
||||||
|
if ((s_sb_write_beats[0] != u_apu_bfm.planned_write_beats) ||
|
||||||
|
(s_sb_write_beats[1] != u_sdio0_bfm.planned_write_beats) ||
|
||||||
|
(s_sb_write_beats[2] != u_usb2_bfm.planned_write_beats)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A write beat attribution mismatch");
|
||||||
|
end
|
||||||
|
if ((s_sb_read_bursts[0] != u_apu_bfm.planned_read_bursts) ||
|
||||||
|
(s_sb_read_bursts[1] != u_sdio0_bfm.planned_read_bursts) ||
|
||||||
|
(s_sb_read_bursts[2] != u_usb2_bfm.planned_read_bursts) ||
|
||||||
|
(s_sb_write_bursts[0] != u_apu_bfm.planned_write_bursts) ||
|
||||||
|
(s_sb_write_bursts[1] != u_sdio0_bfm.planned_write_bursts) ||
|
||||||
|
(s_sb_write_bursts[2] != u_usb2_bfm.planned_write_bursts)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A burst count attribution mismatch");
|
||||||
|
end
|
||||||
|
for (int client = 0; client < 3; client++) begin
|
||||||
|
if ((s_sb_read_beats[client] != u_downstream.read_beats[client]) ||
|
||||||
|
(s_sb_write_beats[client] != u_downstream.write_beats[client])) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A downstream counter mismatch for client %0d", client);
|
||||||
|
end
|
||||||
|
if ((s_sb_read_beats[client] == 0) || (s_sb_write_beats[client] == 0)) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A client %0d made no forward progress", client);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
total_beats = s_sb_read_beats[0] + s_sb_read_beats[1] + s_sb_read_beats[2] +
|
||||||
|
s_sb_write_beats[0] + s_sb_write_beats[1] + s_sb_write_beats[2];
|
||||||
|
if (total_beats < 700) begin
|
||||||
|
$fatal(1, "APU-P8 Gateway A sustained traffic below expectation: %0d beats", total_beats);
|
||||||
|
end
|
||||||
|
$display(
|
||||||
|
"APU-P8 Gateway A contention passed: PASS seed=%0d read_beats=%0d/%0d/%0d write_beats=%0d/%0d/%0d read_bursts=%0d/%0d/%0d write_bursts=%0d/%0d/%0d total_bytes=%0d max_foreign_grants=%0d starvation_bound_arbitrations=3",
|
||||||
|
seed, s_sb_read_beats[0], s_sb_read_beats[1], s_sb_read_beats[2], s_sb_write_beats[0],
|
||||||
|
s_sb_write_beats[1], s_sb_write_beats[2], s_sb_read_bursts[0], s_sb_read_bursts[1],
|
||||||
|
s_sb_read_bursts[2], s_sb_write_bursts[0], s_sb_write_bursts[1], s_sb_write_bursts[2],
|
||||||
|
4 * total_beats, s_max_wait_q);
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
repeat (150000) @(posedge clk_i);
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"APU-P8 Gateway A contention timed out: done=%b%b%b state=%0d rd_active=%b wr_active=%b waits=%0d/%0d/%0d",
|
||||||
|
s_apu_done, s_sdio0_done, s_usb2_done, u_dut.s_state_q, s_rd_active_q, s_wr_active_q,
|
||||||
|
s_wait_q[0], s_wait_q[1], s_wait_q[2]);
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
|
||||||
|
// SPDX-License-Identifier: MulanPSL-2.0
|
||||||
|
|
||||||
|
// APU-P8 quiesced loader admission regression: with quiesce_i asserted, the
|
||||||
|
// owner LP, and the core idle, both COMMAND.MICROCODE_LOAD and
|
||||||
|
// COMMAND.MODEL_LOAD must be admitted and run to completion WITHOUT quiesce
|
||||||
|
// ever being released. The pre-fix apb4_apu quiesce term
|
||||||
|
// (s_dma_admission_block && s_mc_idle) kept apu_dma quiesced while the KWS
|
||||||
|
// model loader was busy, so its fetch DMA never issued a beat and the load
|
||||||
|
// hung until the firmware poll budget expired (HAL RS_ETIMEOUT). This bench
|
||||||
|
// fails closed on the fixed design if either load stalls, if the completion
|
||||||
|
// state/CRC is wrong, or if the image bytes did not cross the AXI read channel
|
||||||
|
// while quiesce was held.
|
||||||
|
|
||||||
|
`include "apu_define.svh"
|
||||||
|
|
||||||
|
module apu_p8_quiesce_load_tb;
|
||||||
|
localparam logic [31:0] ApuBase = 32'h1001_3000;
|
||||||
|
localparam logic [31:0] ImageBase = 32'h3000_0000;
|
||||||
|
localparam logic [31:0] ModelBase = 32'h3001_0000;
|
||||||
|
localparam logic [31:0] ModelBytes = 32'd32768;
|
||||||
|
localparam logic [63:0] MaxCycles = 64'd2000000;
|
||||||
|
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic quiesce_q;
|
||||||
|
logic [31:0] image [0:16383];
|
||||||
|
logic [31:0] model [ 0:8191];
|
||||||
|
logic read_active_q;
|
||||||
|
logic [31:0] read_addr_q;
|
||||||
|
logic [7:0] read_len_q, read_beat_q;
|
||||||
|
logic [63:0] cycle_q;
|
||||||
|
logic [63:0] quiesced_beats_q;
|
||||||
|
logic [63:0] beats_before;
|
||||||
|
logic [31:0] s_value;
|
||||||
|
|
||||||
|
string apumc_path, apum_path;
|
||||||
|
|
||||||
|
apb4_if apb4 (
|
||||||
|
.pclk (clk_i),
|
||||||
|
.presetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) axi4 (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) dma_tx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) dma_rx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) i2s_tx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
axi4_stream_if #(
|
||||||
|
.DATA_WIDTH(32)
|
||||||
|
) i2s_rx_axis (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
// Ready-memory AXI read model: no injected stalls, back-to-back beats.
|
||||||
|
assign axi4.arready = !read_active_q;
|
||||||
|
assign axi4.rid = 1'b0;
|
||||||
|
assign axi4.rdata = (read_addr_q >= ModelBase) ? model[(read_addr_q-ModelBase)>>2] :
|
||||||
|
image[(read_addr_q-ImageBase)>>2];
|
||||||
|
assign axi4.rresp = 2'd0;
|
||||||
|
assign axi4.rlast = read_beat_q == read_len_q;
|
||||||
|
assign axi4.ruser = 1'b0;
|
||||||
|
assign axi4.rvalid = read_active_q;
|
||||||
|
assign axi4.awready = 1'b1;
|
||||||
|
assign axi4.wready = 1'b0;
|
||||||
|
assign axi4.bid = 1'b0;
|
||||||
|
assign axi4.bresp = 2'd0;
|
||||||
|
assign axi4.buser = 1'b0;
|
||||||
|
assign axi4.bvalid = 1'b0;
|
||||||
|
|
||||||
|
assign dma_tx_axis.tdata = 32'd0;
|
||||||
|
assign dma_tx_axis.tkeep = 4'hf;
|
||||||
|
assign dma_tx_axis.tstrb = 4'hf;
|
||||||
|
assign dma_tx_axis.tlast = 1'b0;
|
||||||
|
assign dma_tx_axis.tid = '0;
|
||||||
|
assign dma_tx_axis.tdest = '0;
|
||||||
|
assign dma_tx_axis.tuser = '0;
|
||||||
|
assign dma_tx_axis.tvalid = 1'b0;
|
||||||
|
assign dma_rx_axis.tready = 1'b1;
|
||||||
|
|
||||||
|
assign i2s_tx_axis.tready = 1'b1;
|
||||||
|
|
||||||
|
assign i2s_rx_axis.tdata = 32'd0;
|
||||||
|
assign i2s_rx_axis.tkeep = 4'hf;
|
||||||
|
assign i2s_rx_axis.tstrb = 4'hf;
|
||||||
|
assign i2s_rx_axis.tlast = 1'b0;
|
||||||
|
assign i2s_rx_axis.tid = '0;
|
||||||
|
assign i2s_rx_axis.tdest = '0;
|
||||||
|
assign i2s_rx_axis.tuser = '0;
|
||||||
|
assign i2s_rx_axis.tvalid = 1'b0;
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
read_active_q <= 1'b0;
|
||||||
|
read_addr_q <= 32'd0;
|
||||||
|
read_len_q <= 8'd0;
|
||||||
|
read_beat_q <= 8'd0;
|
||||||
|
cycle_q <= 64'd0;
|
||||||
|
quiesced_beats_q <= 64'd0;
|
||||||
|
end else begin
|
||||||
|
cycle_q <= cycle_q + 1'b1;
|
||||||
|
if (axi4.arvalid && axi4.arready) begin
|
||||||
|
read_active_q <= 1'b1;
|
||||||
|
read_addr_q <= axi4.araddr;
|
||||||
|
read_len_q <= axi4.arlen;
|
||||||
|
read_beat_q <= 8'd0;
|
||||||
|
end
|
||||||
|
if (axi4.rvalid && axi4.rready) begin
|
||||||
|
if (quiesce_q) quiesced_beats_q <= quiesced_beats_q + 1'b1;
|
||||||
|
if (axi4.rlast) read_active_q <= 1'b0;
|
||||||
|
else begin
|
||||||
|
read_addr_q <= read_addr_q + 32'd4;
|
||||||
|
read_beat_q <= read_beat_q + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (axi4.awvalid && axi4.awready)
|
||||||
|
$fatal(1, "P8 quiesce load unexpected AXI write %h", axi4.awaddr);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
apb4_apu #(
|
||||||
|
.EnableP7(1'b1)
|
||||||
|
) u_dut (
|
||||||
|
.clk_i,
|
||||||
|
.rst_n_i,
|
||||||
|
.owner_i (2'd0),
|
||||||
|
.owner_lock_i (1'b0),
|
||||||
|
.quiesce_i (quiesce_q),
|
||||||
|
.resource_reset_i (1'b0),
|
||||||
|
.bridge_epoch_i (8'd0),
|
||||||
|
.i2s_tx_underrun_i (1'b0),
|
||||||
|
.i2s_rx_overrun_i (1'b0),
|
||||||
|
.i2s_rx_flush_busy_i(1'b0),
|
||||||
|
.apb4,
|
||||||
|
.axi4,
|
||||||
|
.dma_tx_axis,
|
||||||
|
.dma_rx_axis,
|
||||||
|
.i2s_tx_axis,
|
||||||
|
.i2s_rx_axis,
|
||||||
|
.idle_o (),
|
||||||
|
.irq_o ()
|
||||||
|
);
|
||||||
|
|
||||||
|
task automatic apb_write(input logic [11:0] offset_i, input logic [31:0] value_i);
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.paddr = ApuBase + offset_i;
|
||||||
|
apb4.psel = 1'b1;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b1;
|
||||||
|
apb4.pwdata = value_i;
|
||||||
|
apb4.pstrb = 4'hf;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.penable = 1'b1;
|
||||||
|
while (!apb4.pready) @(negedge clk_i);
|
||||||
|
if (apb4.pslverr) $fatal(1, "P8 quiesce load APB write %h = %h failed", offset_i, value_i);
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic apb_read(input logic [11:0] offset_i, output logic [31:0] value_o);
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.paddr = ApuBase + offset_i;
|
||||||
|
apb4.psel = 1'b1;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b0;
|
||||||
|
apb4.pstrb = 4'd0;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.penable = 1'b1;
|
||||||
|
while (!apb4.pready) @(negedge clk_i);
|
||||||
|
if (apb4.pslverr) $fatal(1, "P8 quiesce load APB read failed %h", offset_i);
|
||||||
|
value_o = apb4.prdata;
|
||||||
|
@(negedge clk_i);
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
if (!$value$plusargs("APUMC_HEX=%s", apumc_path)) $fatal(1, "APUMC_HEX plusarg missing");
|
||||||
|
if (!$value$plusargs("APUM_HEX=%s", apum_path)) $fatal(1, "APUM_HEX plusarg missing");
|
||||||
|
$readmemh(apumc_path, image);
|
||||||
|
$readmemh(apum_path, model);
|
||||||
|
|
||||||
|
apb4.paddr = 32'd0;
|
||||||
|
apb4.pprot = 3'd0;
|
||||||
|
apb4.psel = 1'b0;
|
||||||
|
apb4.penable = 1'b0;
|
||||||
|
apb4.pwrite = 1'b0;
|
||||||
|
apb4.pwdata = 32'd0;
|
||||||
|
apb4.pstrb = 4'd0;
|
||||||
|
// Quiesce is asserted before reset release and is never deasserted: the
|
||||||
|
// whole scenario runs under the frozen LP quiesce discipline.
|
||||||
|
quiesce_q = 1'b1;
|
||||||
|
repeat (4) @(posedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
repeat (2) @(posedge clk_i);
|
||||||
|
|
||||||
|
// LP-owned, quiesced, idle: the exact state the frozen release flow loads in.
|
||||||
|
apb_read(`APB4_APU__OWNER_STATUS, s_value);
|
||||||
|
if ((s_value[`APB4_APU__OWNER_STATUS_OWNER+1:`APB4_APU__OWNER_STATUS_OWNER] != 2'd0) ||
|
||||||
|
!s_value[`APB4_APU__OWNER_STATUS_QUIESCE] || s_value[`APB4_APU__OWNER_STATUS_RESET])
|
||||||
|
$fatal(1, "P8 quiesce load owner state mismatch %h", s_value);
|
||||||
|
apb_read(`APB4_APU__STATUS, s_value);
|
||||||
|
if (!s_value[`APB4_APU__STATUS_IDLE]) $fatal(1, "P8 quiesce load core not idle %h", s_value);
|
||||||
|
|
||||||
|
apb_write(`APB4_APU__READ_BASE, ImageBase);
|
||||||
|
apb_write(`APB4_APU__READ_LIMIT, ModelBase + ModelBytes - 32'd1);
|
||||||
|
|
||||||
|
// MICROCODE_LOAD under quiesce: the microcode loader was always exempt
|
||||||
|
// from the DMA quiesce while it is itself busy; this leg must keep working.
|
||||||
|
apb_write(`APB4_APU__MC_IMAGE_ADDRESS, ImageBase);
|
||||||
|
apb_write(`APB4_APU__MC_IMAGE_SIZE, image[2]);
|
||||||
|
apb_write(`APB4_APU__MC_EXPECTED_CRC, image[11]);
|
||||||
|
beats_before = quiesced_beats_q;
|
||||||
|
apb_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MICROCODE_LOAD);
|
||||||
|
for (int poll = 0; poll < 200000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__MC_STATUS, s_value);
|
||||||
|
if (s_value[`APB4_APU__MC_STATUS_VALID]) break;
|
||||||
|
if (!s_value[`APB4_APU__MC_STATUS_BUSY])
|
||||||
|
$fatal(1, "P8 quiesce load microcode load failed %h", s_value);
|
||||||
|
end
|
||||||
|
if (!s_value[`APB4_APU__MC_STATUS_VALID])
|
||||||
|
$fatal(1, "P8 quiesce load microcode load timeout (DMA not admitted under quiesce)");
|
||||||
|
apb_read(`APB4_APU__MC_LOCK, s_value);
|
||||||
|
if (!s_value[`APB4_APU__MC_LOCK_LOCKED]) $fatal(1, "P8 quiesce load microcode not locked");
|
||||||
|
apb_read(`APB4_APU__MC_ACTUAL_CRC, s_value);
|
||||||
|
if (s_value != image[11])
|
||||||
|
$fatal(1, "P8 quiesce load microcode CRC mismatch actual=%h expected=%h", s_value, image[11]);
|
||||||
|
if (quiesced_beats_q - beats_before < {32'd0, image[2]} >> 2)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P8 quiesce load microcode bytes did not cross AXI under quiesce beats=%0d",
|
||||||
|
quiesced_beats_q - beats_before
|
||||||
|
);
|
||||||
|
$display("P8_QUIESCE_MC crc=%h beats=%0d", s_value, quiesced_beats_q - beats_before);
|
||||||
|
|
||||||
|
apb_read(`APB4_APU__STATUS, s_value);
|
||||||
|
if (!s_value[`APB4_APU__STATUS_IDLE])
|
||||||
|
$fatal(1, "P8 quiesce load core not idle before model load %h", s_value);
|
||||||
|
|
||||||
|
// MODEL_LOAD under quiesce: the APU-P8 regression. Before the apb4_apu fix
|
||||||
|
// the KWS model loader's fetch DMA stayed quiesced here and the load never
|
||||||
|
// completed; the fixed admission term lets it run while quiesce holds.
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_ADDRESS, ModelBase);
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_SIZE, ModelBytes);
|
||||||
|
apb_write(`APB4_APU__KWS_MODEL_EXPECTED_CRC, `APB4_APU__APUM_PAYLOAD_CRC);
|
||||||
|
beats_before = quiesced_beats_q;
|
||||||
|
apb_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MODEL_LOAD);
|
||||||
|
for (int poll = 0; poll < 200000; poll++) begin
|
||||||
|
apb_read(`APB4_APU__KWS_MODEL_STATUS, s_value);
|
||||||
|
if (!s_value[0]) break;
|
||||||
|
end
|
||||||
|
if (s_value[0])
|
||||||
|
$fatal(1, "P8 quiesce load model load timeout (loader DMA not admitted under quiesce)");
|
||||||
|
if (s_value != 32'h0000_0006) begin
|
||||||
|
apb_read(`APB4_APU__ERROR_STATUS, s_value);
|
||||||
|
$display("P8 quiesce load model load error status=%h", s_value);
|
||||||
|
apb_read(`APB4_APU__ERROR_DETAIL, s_value);
|
||||||
|
$fatal(1, "P8 quiesce load model load failed detail=%h", s_value);
|
||||||
|
end
|
||||||
|
apb_read(`APB4_APU__KWS_MODEL_ACTUAL_CRC, s_value);
|
||||||
|
if (s_value != `APB4_APU__APUM_PAYLOAD_CRC)
|
||||||
|
$fatal(1, "P8 quiesce load model CRC mismatch actual=%h", s_value);
|
||||||
|
if (quiesced_beats_q - beats_before < ModelBytes >> 2)
|
||||||
|
$fatal(
|
||||||
|
1,
|
||||||
|
"P8 quiesce load model bytes did not cross AXI under quiesce beats=%0d",
|
||||||
|
quiesced_beats_q - beats_before
|
||||||
|
);
|
||||||
|
$display("P8_QUIESCE_MODEL crc=%h beats=%0d", s_value, quiesced_beats_q - beats_before);
|
||||||
|
|
||||||
|
// Quiesce was never released: the owner view must still show LP+quiesce and
|
||||||
|
// the core must be idle with clean error state.
|
||||||
|
apb_read(`APB4_APU__OWNER_STATUS, s_value);
|
||||||
|
if ((s_value[`APB4_APU__OWNER_STATUS_OWNER+1:`APB4_APU__OWNER_STATUS_OWNER] != 2'd0) ||
|
||||||
|
!s_value[`APB4_APU__OWNER_STATUS_QUIESCE] || s_value[`APB4_APU__OWNER_STATUS_RESET])
|
||||||
|
$fatal(1, "P8 quiesce load final owner state mismatch %h", s_value);
|
||||||
|
apb_read(`APB4_APU__STATUS, s_value);
|
||||||
|
if (!s_value[`APB4_APU__STATUS_IDLE]) $fatal(1, "P8 quiesce load core busy at end %h", s_value);
|
||||||
|
apb_read(`APB4_APU__ERROR_STATUS, s_value);
|
||||||
|
if (s_value != 32'd0) $fatal(1, "P8 quiesce load first-error status set %h", s_value);
|
||||||
|
|
||||||
|
$display("APU-P8 quiesced loader admission test passed");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
wait (cycle_q > MaxCycles);
|
||||||
|
$fatal(1, "P8 quiesce load global timeout");
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
+17
-15
@@ -97,7 +97,9 @@ module apu_reg_tb;
|
|||||||
|
|
||||||
always #5 clk_i = ~clk_i;
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
apb4_apu u_dut (
|
apb4_apu #(
|
||||||
|
.EnableP7(1'b1)
|
||||||
|
) u_dut (
|
||||||
.clk_i (clk_i),
|
.clk_i (clk_i),
|
||||||
.rst_n_i (rst_n_i),
|
.rst_n_i (rst_n_i),
|
||||||
.owner_i (owner_i),
|
.owner_i (owner_i),
|
||||||
@@ -193,7 +195,7 @@ module apu_reg_tb;
|
|||||||
s_reg_count = 0;
|
s_reg_count = 0;
|
||||||
add_register(`APB4_APU__IP_ID, AccessRo, 32'h4150_5530, 1'b0);
|
add_register(`APB4_APU__IP_ID, AccessRo, 32'h4150_5530, 1'b0);
|
||||||
add_register(`APB4_APU__IP_VERSION, AccessRo, 32'h0001_0001, 1'b0);
|
add_register(`APB4_APU__IP_VERSION, AccessRo, 32'h0001_0001, 1'b0);
|
||||||
add_register(`APB4_APU__CAPABILITY0, AccessRo, 32'h0000_01bd, 1'b0);
|
add_register(`APB4_APU__CAPABILITY0, AccessRo, 32'h0000_01fd, 1'b0);
|
||||||
add_register(`APB4_APU__CAPABILITY1, AccessRo, 32'h0182_7020, 1'b0);
|
add_register(`APB4_APU__CAPABILITY1, AccessRo, 32'h0182_7020, 1'b0);
|
||||||
add_register(`APB4_APU__COMMAND, AccessWo, 32'd0, 1'b1);
|
add_register(`APB4_APU__COMMAND, AccessWo, 32'd0, 1'b1);
|
||||||
add_register(`APB4_APU__STATUS, AccessRo, 32'h0000_0100, 1'b0);
|
add_register(`APB4_APU__STATUS, AccessRo, 32'h0000_0100, 1'b0);
|
||||||
@@ -212,7 +214,7 @@ module apu_reg_tb;
|
|||||||
add_register(`APB4_APU__WRITE_BASE, AccessRw, 32'hffff_ffff, 1'b1);
|
add_register(`APB4_APU__WRITE_BASE, AccessRw, 32'hffff_ffff, 1'b1);
|
||||||
add_register(`APB4_APU__WRITE_LIMIT, AccessRw, 32'd0, 1'b1);
|
add_register(`APB4_APU__WRITE_LIMIT, AccessRw, 32'd0, 1'b1);
|
||||||
add_register(`APB4_APU__DMA_TIMEOUT, AccessRw, 32'h0000_ffff, 1'b0);
|
add_register(`APB4_APU__DMA_TIMEOUT, AccessRw, 32'h0000_ffff, 1'b0);
|
||||||
add_register(`APB4_APU__ABI_DIGEST, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__ABI_DIGEST, AccessRo, 32'hf500_5d7c, 1'b0);
|
||||||
add_register(`APB4_APU__SEQUENCER_STATUS, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__SEQUENCER_STATUS, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__SEQUENCER_RETIRED, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__SEQUENCER_RETIRED, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__STREAM_WATERMARK, AccessRw, 32'd0, 1'b0);
|
add_register(`APB4_APU__STREAM_WATERMARK, AccessRw, 32'd0, 1'b0);
|
||||||
@@ -265,7 +267,7 @@ module apu_reg_tb;
|
|||||||
add_register(`APB4_APU__KWS_OVERRUN_COUNT, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__KWS_OVERRUN_COUNT, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__KWS_MODEL_STATUS, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__KWS_MODEL_STATUS, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__KWS_MODEL_ACTUAL_CRC, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__KWS_MODEL_ACTUAL_CRC, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__KWS_INPUT_CONFIG, AccessRo, 32'h0104_bb80, 1'b0);
|
add_register(`APB4_APU__KWS_INPUT_CONFIG, AccessRw, 32'h0104_bb80, 1'b0);
|
||||||
add_register(`APB4_APU__PERF_CONTROL, AccessRw, 32'd0, 1'b0);
|
add_register(`APB4_APU__PERF_CONTROL, AccessRw, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__PERF_STATUS, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__PERF_STATUS, AccessRo, 32'd0, 1'b0);
|
||||||
add_register(`APB4_APU__PERF_ACTIVE_CYCLES_LO, AccessRo, 32'd0, 1'b0);
|
add_register(`APB4_APU__PERF_ACTIVE_CYCLES_LO, AccessRo, 32'd0, 1'b0);
|
||||||
@@ -428,8 +430,7 @@ module apu_reg_tb;
|
|||||||
logic [31:0] s_expected;
|
logic [31:0] s_expected;
|
||||||
begin
|
begin
|
||||||
for (int unsigned register_index = 0; register_index < RegisterCount; register_index++) begin
|
for (int unsigned register_index = 0; register_index < RegisterCount; register_index++) begin
|
||||||
if ((s_reg_access[register_index] != AccessWo) &&
|
if (s_reg_access[register_index] != AccessWo) begin
|
||||||
(s_reg_offset[register_index] != `APB4_APU__KWS_INPUT_CONFIG)) begin
|
|
||||||
s_expected = preserve_acl_i ?
|
s_expected = preserve_acl_i ?
|
||||||
reset_or_preserved_value(s_reg_offset[register_index], s_reg_reset[register_index]) :
|
reset_or_preserved_value(s_reg_offset[register_index], s_reg_reset[register_index]) :
|
||||||
s_reg_reset[register_index];
|
s_reg_reset[register_index];
|
||||||
@@ -492,8 +493,6 @@ module apu_reg_tb;
|
|||||||
hard_reset();
|
hard_reset();
|
||||||
if (s_reg_access[register_index] == AccessWo) begin
|
if (s_reg_access[register_index] == AccessWo) begin
|
||||||
apb_read(s_reg_offset[register_index], s_value, 1'b1);
|
apb_read(s_reg_offset[register_index], s_value, 1'b1);
|
||||||
end else if (s_reg_offset[register_index] == `APB4_APU__KWS_INPUT_CONFIG) begin
|
|
||||||
apb_read(s_reg_offset[register_index], s_value, 1'b1);
|
|
||||||
end else begin
|
end else begin
|
||||||
expect_read(s_reg_offset[register_index], s_reg_reset[register_index]);
|
expect_read(s_reg_offset[register_index], s_reg_reset[register_index]);
|
||||||
end
|
end
|
||||||
@@ -506,9 +505,7 @@ module apu_reg_tb;
|
|||||||
apb_write(`APB4_APU__SEQUENCER_TIMEOUT, 32'h1234_5678, 4'hf, 1'b0);
|
apb_write(`APB4_APU__SEQUENCER_TIMEOUT, 32'h1234_5678, 4'hf, 1'b0);
|
||||||
apb_write(s_reg_offset[register_index], 32'd0, 4'hf, 1'b1);
|
apb_write(s_reg_offset[register_index], 32'd0, 4'hf, 1'b1);
|
||||||
expect_read(`APB4_APU__SEQUENCER_TIMEOUT, 32'h1234_5678);
|
expect_read(`APB4_APU__SEQUENCER_TIMEOUT, 32'h1234_5678);
|
||||||
if (s_reg_offset[register_index] == `APB4_APU__KWS_INPUT_CONFIG) begin
|
if ((s_reg_offset[register_index] != `APB4_APU__ERROR_ADDRESS) &&
|
||||||
apb_read(s_reg_offset[register_index], s_value, 1'b1);
|
|
||||||
end else if ((s_reg_offset[register_index] != `APB4_APU__ERROR_ADDRESS) &&
|
|
||||||
(s_reg_offset[register_index] != `APB4_APU__ERROR_DETAIL)) begin
|
(s_reg_offset[register_index] != `APB4_APU__ERROR_DETAIL)) begin
|
||||||
expect_read(s_reg_offset[register_index], s_reg_reset[register_index]);
|
expect_read(s_reg_offset[register_index], s_reg_reset[register_index]);
|
||||||
end
|
end
|
||||||
@@ -567,7 +564,7 @@ module apu_reg_tb;
|
|||||||
check_rejected_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_RING_KICK,
|
check_rejected_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_RING_KICK,
|
||||||
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MODEL_LOAD,
|
check_rejected_write(`APB4_APU__COMMAND, 32'd1 << `APB4_APU__COMMAND_MODEL_LOAD,
|
||||||
`APB4_APU__ERROR_CODE_UNSUPPORTED);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__COMMAND, 32'h0000_0010, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
check_rejected_write(`APB4_APU__COMMAND, 32'h0000_0010, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__IRQ_STATE, 32'h0000_0800, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
check_rejected_write(`APB4_APU__IRQ_STATE, 32'h0000_0800, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__IRQ_ENABLE, 32'h0000_0800,
|
check_rejected_write(`APB4_APU__IRQ_ENABLE, 32'h0000_0800,
|
||||||
@@ -579,7 +576,9 @@ module apu_reg_tb;
|
|||||||
hard_reset();
|
hard_reset();
|
||||||
apb_write(`APB4_APU__STREAM_ROUTE, 32'h0000_0001, 4'hf, 1'b0);
|
apb_write(`APB4_APU__STREAM_ROUTE, 32'h0000_0001, 4'hf, 1'b0);
|
||||||
expect_read(`APB4_APU__STREAM_ROUTE, 32'h0000_0001);
|
expect_read(`APB4_APU__STREAM_ROUTE, 32'h0000_0001);
|
||||||
check_rejected_write(`APB4_APU__STREAM_ROUTE, 32'h0000_0004, `APB4_APU__ERROR_CODE_UNSUPPORTED);
|
hard_reset();
|
||||||
|
apb_write(`APB4_APU__STREAM_ROUTE, 32'h0000_0004, 4'hf, 1'b0);
|
||||||
|
expect_read(`APB4_APU__STREAM_ROUTE, 32'h0000_0004);
|
||||||
check_rejected_write(`APB4_APU__STREAM_WATERMARK, 32'h0000_0041,
|
check_rejected_write(`APB4_APU__STREAM_WATERMARK, 32'h0000_0041,
|
||||||
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__STREAM_WATERMARK, 32'h0001_0000,
|
check_rejected_write(`APB4_APU__STREAM_WATERMARK, 32'h0001_0000,
|
||||||
@@ -606,8 +605,11 @@ module apu_reg_tb;
|
|||||||
check_rejected_write(`APB4_APU__KWS_MODEL_ADDRESS, 32'h3005_0001,
|
check_rejected_write(`APB4_APU__KWS_MODEL_ADDRESS, 32'h3005_0001,
|
||||||
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__KWS_MODEL_SIZE, 32'd0, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
check_rejected_write(`APB4_APU__KWS_MODEL_SIZE, 32'd0, `APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__KWS_CONTROL, 32'h0000_0001, `APB4_APU__ERROR_CODE_UNSUPPORTED);
|
check_rejected_write(`APB4_APU__KWS_CONTROL, 32'h0000_0001,
|
||||||
check_rejected_write(`APB4_APU__KWS_CONTROL, 32'h0000_0002, `APB4_APU__ERROR_CODE_UNSUPPORTED);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
|
hard_reset();
|
||||||
|
apb_write(`APB4_APU__KWS_CONTROL, 32'h0000_0002, 4'hf, 1'b0);
|
||||||
|
expect_read(`APB4_APU__KWS_CONTROL, 32'h0000_0002);
|
||||||
check_rejected_write(`APB4_APU__KWS_CONTROL, 32'h0000_0008,
|
check_rejected_write(`APB4_APU__KWS_CONTROL, 32'h0000_0008,
|
||||||
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
`APB4_APU__ERROR_CODE_INVALID_CONFIG);
|
||||||
check_rejected_write(`APB4_APU__KWS_CONFIG, 32'h0000_0080,
|
check_rejected_write(`APB4_APU__KWS_CONFIG, 32'h0000_0080,
|
||||||
|
|||||||
@@ -0,0 +1,522 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
`include "axi4_define.svh"
|
||||||
|
`include "rib_defs.svh"
|
||||||
|
|
||||||
|
/* APU-P8 post-fault visibility reproduction at the LP interconnect level.
|
||||||
|
*
|
||||||
|
* Mirrors the SoC wiring of axi4_bus: master 0 is the LP (management) core and
|
||||||
|
* master 6 is the HP MMIO path (the legacy "usb2" port of axi4_bus). Target 0
|
||||||
|
* is TARGET_CFG (APB4 peripheral window hosting the APU registers at
|
||||||
|
* 0x10013000, UART1 at 0x10018000 and the HP mailbox at 0x10019000), target 3
|
||||||
|
* is TARGET_SDRAM, target 7/8 are the DECERR/SLVERR error slaves exactly as
|
||||||
|
* axi4_bus wires them.
|
||||||
|
*
|
||||||
|
* Sequence under test (MINI_PRODUCT, i.e. the apu_lp_only guard active):
|
||||||
|
* leg A: HP writes the mailbox window and LP reads it back (no fault yet)
|
||||||
|
* leg B: HP write-then-LP-read SDRAM round trip (no fault yet)
|
||||||
|
* leg C: HP stores to the LP-only APU register 0x10013040 -> SLVERR
|
||||||
|
* leg D: HP writes mailbox HP_EVENT/HP_ARG0/HP_SEQUENCE/HP_DOORBELL again
|
||||||
|
* and LP reads them back (the leg that fails at SoC level)
|
||||||
|
* leg E: HP writes the UART1 window post-fault (known to work on SoC)
|
||||||
|
* leg F: HP writes a legal APU offset post-fault (known to work on SoC)
|
||||||
|
* leg G: HP write-then-read SDRAM round trip post-fault
|
||||||
|
*/
|
||||||
|
module axi4_interconnect_hp_postfault_tb;
|
||||||
|
localparam int NumMasters = 8;
|
||||||
|
localparam int NumTargets = 10;
|
||||||
|
|
||||||
|
localparam int TARGET_CFG = 0;
|
||||||
|
localparam int TARGET_SDRAM = 3;
|
||||||
|
localparam int TARGET_DECERR = 7;
|
||||||
|
localparam int TARGET_SLVERR = 8;
|
||||||
|
|
||||||
|
localparam logic [31:0] APU_LP_ONLY_ADDR = 32'h1001_3040;
|
||||||
|
localparam logic [31:0] APU_LEGAL_ADDR = 32'h1001_301C;
|
||||||
|
localparam logic [31:0] UART1_TXDATA = 32'h1001_8010;
|
||||||
|
localparam logic [31:0] MAILBOX_HP_EVENT = 32'h1001_9020;
|
||||||
|
localparam logic [31:0] MAILBOX_HP_ARG0 = 32'h1001_9024;
|
||||||
|
localparam logic [31:0] MAILBOX_HP_SEQUENCE = 32'h1001_9028;
|
||||||
|
localparam logic [31:0] MAILBOX_HP_DOORBELL = 32'h1001_902C;
|
||||||
|
localparam logic [31:0] SDRAM_RESULT = 32'h3BC8_0080;
|
||||||
|
localparam logic [31:0] RESULT_MAGIC = 32'h4852_3031;
|
||||||
|
|
||||||
|
localparam int LP = 0;
|
||||||
|
localparam int HP = 6;
|
||||||
|
|
||||||
|
logic clk_i = 1'b0;
|
||||||
|
logic rst_n_i = 1'b0;
|
||||||
|
logic fault_valid_o;
|
||||||
|
logic [31:0] fault_addr_o;
|
||||||
|
logic [ 3:0] fault_wstrb_o;
|
||||||
|
logic fault_reserved_o;
|
||||||
|
logic fault_access_o;
|
||||||
|
logic [ 2:0] fault_master_o;
|
||||||
|
logic [ 2:0] fault_code_o;
|
||||||
|
logic user_bus_idle_o;
|
||||||
|
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) masters[NumMasters] (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
|
||||||
|
axi4_if #(
|
||||||
|
.ADDR_WIDTH(32),
|
||||||
|
.DATA_WIDTH(32),
|
||||||
|
.ID_WIDTH (1),
|
||||||
|
.USER_WIDTH(1)
|
||||||
|
) targets[NumTargets] (
|
||||||
|
.aclk (clk_i),
|
||||||
|
.aresetn(rst_n_i)
|
||||||
|
);
|
||||||
|
|
||||||
|
always #5 clk_i = ~clk_i;
|
||||||
|
|
||||||
|
axi4_interconnect #(
|
||||||
|
.NumMasters(NumMasters),
|
||||||
|
.NumTargets(NumTargets)
|
||||||
|
) u_dut (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i (rst_n_i),
|
||||||
|
.masters (masters),
|
||||||
|
.targets (targets),
|
||||||
|
.user_bus_enable_i (1'b1),
|
||||||
|
.user_bus_idle_o (user_bus_idle_o),
|
||||||
|
.mem_pad_mode_i (2'd0),
|
||||||
|
.perf_enable_i (1'b0),
|
||||||
|
.perf_clear_i (1'b0),
|
||||||
|
.fault_valid_o (fault_valid_o),
|
||||||
|
.fault_addr_o (fault_addr_o),
|
||||||
|
.fault_wstrb_o (fault_wstrb_o),
|
||||||
|
.fault_reserved_o (fault_reserved_o),
|
||||||
|
.fault_access_o (fault_access_o),
|
||||||
|
.fault_master_o (fault_master_o),
|
||||||
|
.fault_code_o (fault_code_o),
|
||||||
|
.perf_mgmt_wait_o (),
|
||||||
|
.perf_user_wait_o (),
|
||||||
|
.perf_dma_wait_o (),
|
||||||
|
.perf_sdio0_wait_o (),
|
||||||
|
.perf_sdio1_wait_o (),
|
||||||
|
.perf_usb2_wait_o (),
|
||||||
|
.perf_apb4_periph_wait_o(),
|
||||||
|
.perf_apb4_system_wait_o(),
|
||||||
|
.perf_sdram_wait_o (),
|
||||||
|
.perf_psram_wait_o (),
|
||||||
|
.perf_flash_wait_o (),
|
||||||
|
.perf_opipsram_wait_o ()
|
||||||
|
);
|
||||||
|
|
||||||
|
// TARGET_CFG model: a small write/read store covering the APU, UART1 and
|
||||||
|
// HP-mailbox windows used by the flow. Single outstanding transaction, which
|
||||||
|
// matches how the interconnect serializes each target.
|
||||||
|
logic [31:0] cfg_mem [4096];
|
||||||
|
logic [ 1:0] cfg_fsm_q = 2'd0;
|
||||||
|
logic [31:0] cfg_addr_q = '0;
|
||||||
|
logic cfg_id_q = 1'b0;
|
||||||
|
|
||||||
|
assign targets[TARGET_CFG].awready = (cfg_fsm_q == 2'd0) && !targets[TARGET_CFG].arvalid;
|
||||||
|
assign targets[TARGET_CFG].arready = (cfg_fsm_q == 2'd0);
|
||||||
|
assign targets[TARGET_CFG].wready = (cfg_fsm_q == 2'd1);
|
||||||
|
assign targets[TARGET_CFG].bvalid = (cfg_fsm_q == 2'd2);
|
||||||
|
assign targets[TARGET_CFG].bid = cfg_id_q;
|
||||||
|
assign targets[TARGET_CFG].bresp = `AXI4_RESP_OKAY;
|
||||||
|
assign targets[TARGET_CFG].buser = '0;
|
||||||
|
assign targets[TARGET_CFG].rvalid = (cfg_fsm_q == 2'd3);
|
||||||
|
assign targets[TARGET_CFG].rid = cfg_id_q;
|
||||||
|
assign targets[TARGET_CFG].rdata = cfg_mem[cfg_addr_q[13:2]];
|
||||||
|
assign targets[TARGET_CFG].rresp = `AXI4_RESP_OKAY;
|
||||||
|
assign targets[TARGET_CFG].rlast = 1'b1;
|
||||||
|
assign targets[TARGET_CFG].ruser = '0;
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
cfg_fsm_q <= 2'd0;
|
||||||
|
cfg_addr_q <= '0;
|
||||||
|
cfg_id_q <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
unique case (cfg_fsm_q)
|
||||||
|
2'd0: begin
|
||||||
|
if (targets[TARGET_CFG].arvalid && targets[TARGET_CFG].arready) begin
|
||||||
|
cfg_addr_q <= targets[TARGET_CFG].araddr;
|
||||||
|
cfg_id_q <= targets[TARGET_CFG].arid;
|
||||||
|
cfg_fsm_q <= 2'd3;
|
||||||
|
end else if (targets[TARGET_CFG].awvalid && targets[TARGET_CFG].awready) begin
|
||||||
|
cfg_addr_q <= targets[TARGET_CFG].awaddr;
|
||||||
|
cfg_id_q <= targets[TARGET_CFG].awid;
|
||||||
|
cfg_fsm_q <= 2'd1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
2'd1: begin
|
||||||
|
if (targets[TARGET_CFG].wvalid && targets[TARGET_CFG].wready) begin
|
||||||
|
cfg_mem[cfg_addr_q[13:2]] <= targets[TARGET_CFG].wdata;
|
||||||
|
cfg_fsm_q <= 2'd2;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
2'd2: if (targets[TARGET_CFG].bvalid && targets[TARGET_CFG].bready) cfg_fsm_q <= 2'd0;
|
||||||
|
2'd3: if (targets[TARGET_CFG].rvalid && targets[TARGET_CFG].rready) cfg_fsm_q <= 2'd0;
|
||||||
|
default: cfg_fsm_q <= 2'd0;
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// TARGET_SDRAM model: single-beat word store for the HP->LP round trip.
|
||||||
|
logic [31:0] sdram_mem [4096];
|
||||||
|
logic [ 1:0] sdram_fsm_q = 2'd0;
|
||||||
|
logic [31:0] sdram_addr_q = '0;
|
||||||
|
logic sdram_id_q = 1'b0;
|
||||||
|
|
||||||
|
assign targets[TARGET_SDRAM].awready = (sdram_fsm_q == 2'd0) && !targets[TARGET_SDRAM].arvalid;
|
||||||
|
assign targets[TARGET_SDRAM].arready = (sdram_fsm_q == 2'd0);
|
||||||
|
assign targets[TARGET_SDRAM].wready = (sdram_fsm_q == 2'd1);
|
||||||
|
assign targets[TARGET_SDRAM].bvalid = (sdram_fsm_q == 2'd2);
|
||||||
|
assign targets[TARGET_SDRAM].bid = sdram_id_q;
|
||||||
|
assign targets[TARGET_SDRAM].bresp = `AXI4_RESP_OKAY;
|
||||||
|
assign targets[TARGET_SDRAM].buser = '0;
|
||||||
|
assign targets[TARGET_SDRAM].rvalid = (sdram_fsm_q == 2'd3);
|
||||||
|
assign targets[TARGET_SDRAM].rid = sdram_id_q;
|
||||||
|
assign targets[TARGET_SDRAM].rdata = sdram_mem[sdram_addr_q[13:2]];
|
||||||
|
assign targets[TARGET_SDRAM].rresp = `AXI4_RESP_OKAY;
|
||||||
|
assign targets[TARGET_SDRAM].rlast = 1'b1;
|
||||||
|
assign targets[TARGET_SDRAM].ruser = '0;
|
||||||
|
|
||||||
|
always_ff @(posedge clk_i or negedge rst_n_i) begin
|
||||||
|
if (!rst_n_i) begin
|
||||||
|
sdram_fsm_q <= 2'd0;
|
||||||
|
sdram_addr_q <= '0;
|
||||||
|
sdram_id_q <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
unique case (sdram_fsm_q)
|
||||||
|
2'd0: begin
|
||||||
|
if (targets[TARGET_SDRAM].arvalid && targets[TARGET_SDRAM].arready) begin
|
||||||
|
sdram_addr_q <= targets[TARGET_SDRAM].araddr;
|
||||||
|
sdram_id_q <= targets[TARGET_SDRAM].arid;
|
||||||
|
sdram_fsm_q <= 2'd3;
|
||||||
|
end else if (targets[TARGET_SDRAM].awvalid && targets[TARGET_SDRAM].awready) begin
|
||||||
|
sdram_addr_q <= targets[TARGET_SDRAM].awaddr;
|
||||||
|
sdram_id_q <= targets[TARGET_SDRAM].awid;
|
||||||
|
sdram_fsm_q <= 2'd1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
2'd1: begin
|
||||||
|
if (targets[TARGET_SDRAM].wvalid && targets[TARGET_SDRAM].wready) begin
|
||||||
|
sdram_mem[sdram_addr_q[13:2]] <= targets[TARGET_SDRAM].wdata;
|
||||||
|
sdram_fsm_q <= 2'd2;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
2'd2: if (targets[TARGET_SDRAM].bvalid && targets[TARGET_SDRAM].bready) sdram_fsm_q <= 2'd0;
|
||||||
|
2'd3: if (targets[TARGET_SDRAM].rvalid && targets[TARGET_SDRAM].rready) sdram_fsm_q <= 2'd0;
|
||||||
|
default: sdram_fsm_q <= 2'd0;
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Every other target is an error slave so a mis-decoded access is loud:
|
||||||
|
// target 7/8 keep the axi4_bus DECERR/SLVERR roles, the rest are DECERR.
|
||||||
|
for (genvar target = 0; target < NumTargets; target++) begin : GEN_ERROR_TARGETS
|
||||||
|
if ((target != TARGET_CFG) && (target != TARGET_SDRAM)) begin : gen_err
|
||||||
|
localparam logic [1:0] Response =
|
||||||
|
(target == TARGET_SLVERR) ? `AXI4_RESP_SLAVE_ERROR : `AXI4_RESP_DECODE_ERROR;
|
||||||
|
axi4_error_slave #(
|
||||||
|
.Response(Response)
|
||||||
|
) u_target (
|
||||||
|
.clk_i (clk_i),
|
||||||
|
.rst_n_i(rst_n_i),
|
||||||
|
.axi4 (targets[target])
|
||||||
|
);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
`define INIT_MASTER(index) \
|
||||||
|
masters[index].awid = '0; \
|
||||||
|
masters[index].awaddr = '0; \
|
||||||
|
masters[index].awlen = '0; \
|
||||||
|
masters[index].awsize = `AXI4_BURST_SIZE_4BYTES; \
|
||||||
|
masters[index].awburst = `AXI4_BURST_TYPE_INCR; \
|
||||||
|
masters[index].awlock = `AXI4_LOCK_NORM; \
|
||||||
|
masters[index].awcache = `AXI4_CACHE_NO_BUF; \
|
||||||
|
masters[index].awprot = `AXI4_PROT_DATA; \
|
||||||
|
masters[index].awqos = `AXI4_QOS_NORMAL; \
|
||||||
|
masters[index].awregion = `AXI4_REGION_NORMAL; \
|
||||||
|
masters[index].awuser = '0; \
|
||||||
|
masters[index].awvalid = 1'b0; \
|
||||||
|
masters[index].wdata = '0; \
|
||||||
|
masters[index].wstrb = '0; \
|
||||||
|
masters[index].wlast = 1'b0; \
|
||||||
|
masters[index].wuser = '0; \
|
||||||
|
masters[index].wvalid = 1'b0; \
|
||||||
|
masters[index].bready = 1'b0; \
|
||||||
|
masters[index].arid = '0; \
|
||||||
|
masters[index].araddr = '0; \
|
||||||
|
masters[index].arlen = '0; \
|
||||||
|
masters[index].arsize = `AXI4_BURST_SIZE_4BYTES; \
|
||||||
|
masters[index].arburst = `AXI4_BURST_TYPE_INCR; \
|
||||||
|
masters[index].arlock = `AXI4_LOCK_NORM; \
|
||||||
|
masters[index].arcache = `AXI4_CACHE_NO_BUF; \
|
||||||
|
masters[index].arprot = `AXI4_PROT_DATA; \
|
||||||
|
masters[index].arqos = `AXI4_QOS_NORMAL; \
|
||||||
|
masters[index].arregion = `AXI4_REGION_NORMAL; \
|
||||||
|
masters[index].aruser = '0; \
|
||||||
|
masters[index].arvalid = 1'b0; \
|
||||||
|
masters[index].rready = 1'b0;
|
||||||
|
|
||||||
|
// Bounded single-beat write from the HP-attributed master 6.
|
||||||
|
task automatic hp_write(input logic [31:0] address, input logic [31:0] data,
|
||||||
|
input logic [1:0] expected_response, input string leg);
|
||||||
|
integer cycles;
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].awaddr = address;
|
||||||
|
masters[HP].awlen = 8'd0;
|
||||||
|
masters[HP].awvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: AW handshake wedged at %h", leg, address);
|
||||||
|
end while (!masters[HP].awready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].awvalid = 1'b0;
|
||||||
|
masters[HP].wdata = data;
|
||||||
|
masters[HP].wstrb = 4'hF;
|
||||||
|
masters[HP].wlast = 1'b1;
|
||||||
|
masters[HP].wvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: W handshake wedged at %h", leg, address);
|
||||||
|
end while (!masters[HP].wready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].wvalid = 1'b0;
|
||||||
|
masters[HP].bready = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
while (!masters[HP].bvalid) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: B response wedged at %h", leg, address);
|
||||||
|
end
|
||||||
|
#1;
|
||||||
|
if (masters[HP].bresp != expected_response) begin
|
||||||
|
$fatal(1, "%s: HP write at %h got resp=%0d, expected %0d", leg, address, masters[HP].bresp,
|
||||||
|
expected_response);
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].bready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Bounded single-beat read from the HP-attributed master 6.
|
||||||
|
task automatic hp_read(input logic [31:0] address, input logic [31:0] expected_data,
|
||||||
|
input string leg);
|
||||||
|
integer cycles;
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].araddr = address;
|
||||||
|
masters[HP].arlen = 8'd0;
|
||||||
|
masters[HP].arvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: AR handshake wedged at %h", leg, address);
|
||||||
|
end while (!masters[HP].arready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].arvalid = 1'b0;
|
||||||
|
masters[HP].rready = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
while (!masters[HP].rvalid) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: R response wedged at %h", leg, address);
|
||||||
|
end
|
||||||
|
#1;
|
||||||
|
if ((masters[HP].rresp != `AXI4_RESP_OKAY) || (masters[HP].rdata != expected_data)) begin
|
||||||
|
$fatal(1, "%s: HP read at %h got resp=%0d data=%h, expected OKAY/%h", leg, address,
|
||||||
|
masters[HP].rresp, masters[HP].rdata, expected_data);
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].rready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Bounded single-beat read from the LP (management) master 0.
|
||||||
|
task automatic lp_read(input logic [31:0] address, input logic [31:0] expected_data,
|
||||||
|
input string leg);
|
||||||
|
integer cycles;
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[LP].araddr = address;
|
||||||
|
masters[LP].arlen = 8'd0;
|
||||||
|
masters[LP].arvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: AR handshake wedged at %h", leg, address);
|
||||||
|
end while (!masters[LP].arready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[LP].arvalid = 1'b0;
|
||||||
|
masters[LP].rready = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
while (!masters[LP].rvalid) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: R response wedged at %h", leg, address);
|
||||||
|
end
|
||||||
|
#1;
|
||||||
|
if ((masters[LP].rresp != `AXI4_RESP_OKAY) || (masters[LP].rdata != expected_data)) begin
|
||||||
|
$fatal(1, "%s: LP read at %h got resp=%0d data=%h, expected OKAY/%h", leg, address,
|
||||||
|
masters[LP].rresp, masters[LP].rdata, expected_data);
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[LP].rready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// HP store to the LP-only APU register: the SLVERR response must complete
|
||||||
|
// the AXI handshake and the fault record must classify it as PROTERR.
|
||||||
|
task automatic hp_lp_only_probe(input string leg);
|
||||||
|
integer cycles;
|
||||||
|
begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].awaddr = APU_LP_ONLY_ADDR;
|
||||||
|
masters[HP].awlen = 8'd0;
|
||||||
|
masters[HP].awvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: probe AW wedged", leg);
|
||||||
|
end while (!masters[HP].awready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].awvalid = 1'b0;
|
||||||
|
masters[HP].wdata = 32'h0000_0000;
|
||||||
|
masters[HP].wstrb = 4'hF;
|
||||||
|
masters[HP].wlast = 1'b1;
|
||||||
|
masters[HP].wvalid = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
do begin
|
||||||
|
@(posedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: probe W wedged", leg);
|
||||||
|
end while (!masters[HP].wready);
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].wvalid = 1'b0;
|
||||||
|
masters[HP].bready = 1'b1;
|
||||||
|
cycles = 0;
|
||||||
|
while (!masters[HP].bvalid) begin
|
||||||
|
@(negedge clk_i);
|
||||||
|
cycles = cycles + 1;
|
||||||
|
if (cycles > 64) $fatal(1, "%s: probe B wedged (SLVERR never completed)", leg);
|
||||||
|
end
|
||||||
|
#1;
|
||||||
|
if ((masters[HP].bresp != `AXI4_RESP_SLAVE_ERROR) || !fault_valid_o || !fault_access_o ||
|
||||||
|
(fault_addr_o != APU_LP_ONLY_ADDR) || (fault_wstrb_o != 4'hF) ||
|
||||||
|
(fault_master_o != 3'(HP)) || (fault_code_o != `RIB_RESP_PROTERR)) begin
|
||||||
|
$fatal(1,
|
||||||
|
"%s: probe misclassified: resp=%0d fault=%0d access=%0d addr=%h master=%0d code=%0d",
|
||||||
|
leg, masters[HP].bresp, fault_valid_o, fault_access_o, fault_addr_o, fault_master_o,
|
||||||
|
fault_code_o);
|
||||||
|
end
|
||||||
|
@(negedge clk_i);
|
||||||
|
masters[HP].bready = 1'b0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic publish_mailbox(input logic [31:0] sequence_value, input string leg);
|
||||||
|
begin
|
||||||
|
hp_write(MAILBOX_HP_EVENT, 32'h0000_0002, `AXI4_RESP_OKAY, leg);
|
||||||
|
hp_write(MAILBOX_HP_ARG0, 32'h0000_0005, `AXI4_RESP_OKAY, leg);
|
||||||
|
hp_write(MAILBOX_HP_SEQUENCE, sequence_value, `AXI4_RESP_OKAY, leg);
|
||||||
|
hp_write(MAILBOX_HP_DOORBELL, 32'h0000_0001, `AXI4_RESP_OKAY, leg);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task automatic check_mailbox(input logic [31:0] sequence_value, input string leg);
|
||||||
|
begin
|
||||||
|
lp_read(MAILBOX_HP_SEQUENCE, sequence_value, leg);
|
||||||
|
lp_read(MAILBOX_HP_EVENT, 32'h0000_0002, leg);
|
||||||
|
lp_read(MAILBOX_HP_ARG0, 32'h0000_0005, leg);
|
||||||
|
lp_read(MAILBOX_HP_DOORBELL, 32'h0000_0001, leg);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
integer watchdog;
|
||||||
|
always @(posedge clk_i) begin
|
||||||
|
if (rst_n_i) begin
|
||||||
|
watchdog = watchdog + 1;
|
||||||
|
if (watchdog > 20000) $fatal(1, "global watchdog fired");
|
||||||
|
end else begin
|
||||||
|
watchdog = 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
`INIT_MASTER(0)
|
||||||
|
`INIT_MASTER(1)
|
||||||
|
`INIT_MASTER(2)
|
||||||
|
`INIT_MASTER(3)
|
||||||
|
`INIT_MASTER(4)
|
||||||
|
`INIT_MASTER(5)
|
||||||
|
`INIT_MASTER(6)
|
||||||
|
`INIT_MASTER(7)
|
||||||
|
repeat (4) @(posedge clk_i);
|
||||||
|
rst_n_i = 1'b1;
|
||||||
|
repeat (2) @(posedge clk_i);
|
||||||
|
|
||||||
|
`ifndef MINI_PRODUCT
|
||||||
|
$fatal(1, "this reproduction requires MINI_PRODUCT (the apu_lp_only guard)");
|
||||||
|
`endif
|
||||||
|
|
||||||
|
// Leg A: control — HP mailbox publish round trip with no prior fault.
|
||||||
|
publish_mailbox(32'h0000_0001, "leg A");
|
||||||
|
check_mailbox(32'h0000_0001, "leg A");
|
||||||
|
$display("LEG A PASS: pre-fault HP mailbox writes visible to LP");
|
||||||
|
|
||||||
|
// Leg B: control — HP SDRAM store visible to an LP read.
|
||||||
|
hp_write(SDRAM_RESULT, RESULT_MAGIC, `AXI4_RESP_OKAY, "leg B");
|
||||||
|
lp_read(SDRAM_RESULT, RESULT_MAGIC, "leg B");
|
||||||
|
$display("LEG B PASS: pre-fault HP SDRAM write visible to LP");
|
||||||
|
|
||||||
|
// Leg C: the deliberate LP-only store probe -> SLVERR completes cleanly.
|
||||||
|
hp_lp_only_probe("leg C");
|
||||||
|
$display("LEG C PASS: LP-only APU probe returned SLVERR with PROTERR fault record");
|
||||||
|
|
||||||
|
// Leg D: the SoC failure signature — post-fault HP mailbox publish.
|
||||||
|
publish_mailbox(32'h0000_0009, "leg D");
|
||||||
|
check_mailbox(32'h0000_0009, "leg D");
|
||||||
|
$display("LEG D PASS: post-fault HP mailbox writes visible to LP");
|
||||||
|
|
||||||
|
// Leg E: post-fault UART1 window write (the path that keeps working on SoC).
|
||||||
|
hp_write(UART1_TXDATA, 32'h0000_0041, `AXI4_RESP_OKAY, "leg E");
|
||||||
|
lp_read(UART1_TXDATA, 32'h0000_0041, "leg E");
|
||||||
|
$display("LEG E PASS: post-fault HP UART1-window write visible to LP");
|
||||||
|
|
||||||
|
// Leg F: post-fault write to a legal APU offset still lands.
|
||||||
|
hp_write(APU_LEGAL_ADDR, 32'hDEAD_BEEF, `AXI4_RESP_OKAY, "leg F");
|
||||||
|
lp_read(APU_LEGAL_ADDR, 32'hDEAD_BEEF, "leg F");
|
||||||
|
$display("LEG F PASS: post-fault HP legal-APU-window write visible to LP");
|
||||||
|
|
||||||
|
// Leg G: post-fault SDRAM round trip (HP store, LP read, HP read).
|
||||||
|
hp_write(SDRAM_RESULT, RESULT_MAGIC, `AXI4_RESP_OKAY, "leg G");
|
||||||
|
lp_read(SDRAM_RESULT, RESULT_MAGIC, "leg G");
|
||||||
|
hp_read(SDRAM_RESULT, RESULT_MAGIC, "leg G");
|
||||||
|
$display("LEG G PASS: post-fault HP SDRAM write/read round trip intact");
|
||||||
|
|
||||||
|
// Repeat the probe once more to catch sticky state after a second fault.
|
||||||
|
hp_lp_only_probe("leg H");
|
||||||
|
publish_mailbox(32'h0000_000A, "leg H");
|
||||||
|
check_mailbox(32'h0000_000A, "leg H");
|
||||||
|
$display("LEG H PASS: second probe + publish round trip intact");
|
||||||
|
|
||||||
|
$display("AXI4 interconnect HP post-fault visibility test passed");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
`undef INIT_MASTER
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
"""Differential tests for the committed APU-P7 frontend profile-1 BAM."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from fractions import Fraction
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
import apu_kws_frontend as bam # noqa: E402
|
||||||
|
import generate_apu_kws_rtl_constants as gen # noqa: E402
|
||||||
|
from apu_kws_convert import import_tflite # noqa: E402
|
||||||
|
from run_apu_p7_kws_rtl import ( # noqa: E402
|
||||||
|
PCM_SOURCES,
|
||||||
|
TFLITE,
|
||||||
|
_load_manifest,
|
||||||
|
_padded_pcm,
|
||||||
|
_write_hex,
|
||||||
|
)
|
||||||
|
|
||||||
|
KWS01 = ROOT / ".cache/retrosoc/sources/apu-kws-mfcc/datasets/kws01"
|
||||||
|
ROM = ROOT / "rtl/ip/multimedia/apu_kws_rom.svh"
|
||||||
|
DUMP_TB = ROOT / "tests/rtl/apu_p7_frontend_dump_tb.sv"
|
||||||
|
OFFICIAL_SUBSET = (0, 1, 97, 194, 291, 388, 485, 563, 582, 679, 776, 873, 970)
|
||||||
|
MFCC_PATTERN = re.compile(r"^MFCC_HEX (\d+) ([0-9a-f]{980})$")
|
||||||
|
PEAK_PATTERN = re.compile(r"^PEAK (\d+) (\d+)$")
|
||||||
|
COMPLETE_PATTERN = re.compile(r"^APU_P7_FRONTEND_DUMP_COMPLETE windows=(\d+) first=(\d+)$")
|
||||||
|
_ROM_SIGNED = re.compile(r"\s*\d+'d(\d+):\s*(\w+)\s*=\s*(-)?\d+'sd(\d+);")
|
||||||
|
_ROM_UNSIGNED = re.compile(r"\s*\d+'d(\d+):\s*(\w+)\s*=\s*\d+'d(\d+);")
|
||||||
|
_ROM_LN2 = re.compile(r"ApuKwsLn2Q24\s*=\s*32'sd(\d+);")
|
||||||
|
|
||||||
|
|
||||||
|
def _signed8(value: int) -> int:
|
||||||
|
return value - 256 if value >= 128 else value
|
||||||
|
|
||||||
|
|
||||||
|
def _rom_tables() -> tuple[dict[str, list[int]], int]:
|
||||||
|
tables: dict[str, dict[int, int]] = {}
|
||||||
|
ln2: int | None = None
|
||||||
|
for line in ROM.read_text(encoding="ascii").splitlines():
|
||||||
|
match = _ROM_SIGNED.match(line)
|
||||||
|
if match:
|
||||||
|
index, name, sign, value = match.groups()
|
||||||
|
tables.setdefault(name, {})[int(index)] = -int(value) if sign else int(value)
|
||||||
|
continue
|
||||||
|
match = _ROM_UNSIGNED.match(line)
|
||||||
|
if match:
|
||||||
|
index, name, value = match.groups()
|
||||||
|
tables.setdefault(name, {})[int(index)] = int(value)
|
||||||
|
continue
|
||||||
|
match = _ROM_LN2.search(line)
|
||||||
|
if match:
|
||||||
|
ln2 = int(match.group(1))
|
||||||
|
assert ln2 is not None
|
||||||
|
return {name: [words[i] for i in sorted(words)] for name, words in tables.items()}, ln2
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p7_frontend_bam_constants_match_rom() -> None:
|
||||||
|
"""BAM tables equal the generator outputs and the ROM words the RTL consumes."""
|
||||||
|
tables, ln2 = _rom_tables()
|
||||||
|
twiddle_real, twiddle_imag = bam.twiddle_q30()
|
||||||
|
generator_twiddle_real, generator_twiddle_imag = gen._twiddle()
|
||||||
|
assert bam.hann_q30() == tuple(gen._hann())
|
||||||
|
assert twiddle_real == tuple(generator_twiddle_real)
|
||||||
|
assert twiddle_imag == tuple(generator_twiddle_imag)
|
||||||
|
assert bam.mel_q30() == tuple(gen._mel_weights())
|
||||||
|
assert bam.dct_q30() == tuple(gen._dct())
|
||||||
|
assert bam.log_q24() == tuple(gen._log_rom())
|
||||||
|
assert list(bam.hann_q30()) == tables["apu_kws_hann_q30"]
|
||||||
|
assert list(twiddle_real) == tables["apu_kws_twiddle_real_q30"]
|
||||||
|
assert list(twiddle_imag) == tables["apu_kws_twiddle_imag_q30"]
|
||||||
|
assert list(bam.mel_q30()) == tables["apu_kws_mel_q30"]
|
||||||
|
assert list(bam.dct_q30()) == tables["apu_kws_dct_q30"]
|
||||||
|
assert list(bam.log_q24()) == tables["apu_kws_log_q24"]
|
||||||
|
assert bam.ln2_q24() == ln2
|
||||||
|
scale = struct.unpack("<f", struct.pack("<I", bam.QUANT_SCALE_BITS))[0]
|
||||||
|
assert Fraction(scale) * (1 << 24) == bam.QUANT_DIVISOR
|
||||||
|
assert bam.QUANT_DIVISOR == 0x0095AF17
|
||||||
|
assert bam.QUANT_BIAS * bam.QUANT_DIVISOR == 0x3087C475
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p7_frontend_bam_vs_official_features() -> None:
|
||||||
|
"""BAM tracks the official corpus within +-1 LSB, wrap bytes byte-exact."""
|
||||||
|
if not PCM_SOURCES.is_dir() or not KWS01.is_dir():
|
||||||
|
return
|
||||||
|
records = _load_manifest()
|
||||||
|
for index in OFFICIAL_SUBSET:
|
||||||
|
official_path = KWS01 / records[index]["bin_name"]
|
||||||
|
if not official_path.is_file():
|
||||||
|
return
|
||||||
|
for index in OFFICIAL_SUBSET:
|
||||||
|
stages = bam.frontend_stages(bam.samples_from_pcm(_padded_pcm(records[index])))
|
||||||
|
official = (KWS01 / records[index]["bin_name"]).read_bytes()
|
||||||
|
assert len(official) == bam.FEATURE_BYTES
|
||||||
|
assert len(stages.features) == bam.FEATURE_BYTES
|
||||||
|
differing = [
|
||||||
|
(position, actual, expected)
|
||||||
|
for position, (actual, expected) in enumerate(zip(stages.features, official))
|
||||||
|
if actual != expected
|
||||||
|
]
|
||||||
|
for position, actual, expected in differing:
|
||||||
|
assert abs(_signed8(actual) - _signed8(expected)) <= 1, (
|
||||||
|
f"tst index {index} byte {position}: BAM {_signed8(actual)} vs "
|
||||||
|
f"official {_signed8(expected)} exceeds +-1 LSB"
|
||||||
|
)
|
||||||
|
if index == 563:
|
||||||
|
quantized = [value for row in stages.quantized for value in row]
|
||||||
|
wraps = [
|
||||||
|
position
|
||||||
|
for position, value in enumerate(quantized)
|
||||||
|
if value < -128 or value > 127
|
||||||
|
]
|
||||||
|
assert len(wraps) == 15, f"tst_000563 wrap positions {wraps}"
|
||||||
|
assert all(stages.features[position] == official[position] for position in wraps)
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_dump_tb(build_dir: Path) -> dict[str, Path | None]:
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
filelist = build_dir / "apu_p7_frontend_dump.fl"
|
||||||
|
filelist.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"+define+SV_ASSRT_DISABLE",
|
||||||
|
f"+incdir+{common}",
|
||||||
|
f"+incdir+{common / 'interface'}",
|
||||||
|
f"+incdir+{multimedia}",
|
||||||
|
str(common / "interface/axi4_stream_if.sv"),
|
||||||
|
str(multimedia / "apu_kws_sram_client.sv"),
|
||||||
|
str(multimedia / "apu_kws_engine.sv"),
|
||||||
|
str(DUMP_TB),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
converted = build_dir / "apu_p7_frontend_dump.v"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||||
|
"-f",
|
||||||
|
str(filelist),
|
||||||
|
"--output",
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
simv = build_dir / "apu_p7_frontend_dump_simv"
|
||||||
|
subprocess.run(
|
||||||
|
["iverilog", "-g2012", "-s", "apu_p7_frontend_dump_tb", "-o", str(simv), str(converted)],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
verilator_binary: Path | None = None
|
||||||
|
if shutil.which("verilator") is not None:
|
||||||
|
verilator_dir = build_dir / "verilator"
|
||||||
|
ccache_dir = build_dir / "ccache"
|
||||||
|
ccache_dir.mkdir(exist_ok=True)
|
||||||
|
environment = {
|
||||||
|
**os.environ,
|
||||||
|
"CCACHE_DIR": str(ccache_dir),
|
||||||
|
"CCACHE_TEMPDIR": str(ccache_dir),
|
||||||
|
}
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"verilator",
|
||||||
|
"--binary",
|
||||||
|
"--timing",
|
||||||
|
"-O3",
|
||||||
|
"-Wno-fatal",
|
||||||
|
"--top-module",
|
||||||
|
"apu_p7_frontend_dump_tb",
|
||||||
|
"--Mdir",
|
||||||
|
str(verilator_dir),
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
verilator_binary = verilator_dir / "Vapu_p7_frontend_dump_tb"
|
||||||
|
return {"icarus": simv, "verilator": verilator_binary}
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p7_frontend_rtl_differential(tmp_path: Path) -> None:
|
||||||
|
"""RTL frontend MFCC bytes are bit-exact against the committed BAM."""
|
||||||
|
if any(shutil.which(tool) is None for tool in ("iverilog", "vvp", "sv2v")):
|
||||||
|
return
|
||||||
|
if not PCM_SOURCES.is_dir() or not TFLITE.is_file():
|
||||||
|
return
|
||||||
|
verilator = shutil.which("verilator")
|
||||||
|
# Icarus needs ~7 minutes per window on this engine; keep one window there.
|
||||||
|
spans = [(0, 2), (563, 1)] if verilator is not None else [(563, 1)]
|
||||||
|
indices = [index for first, count in spans for index in range(first, first + count)]
|
||||||
|
records = _load_manifest()
|
||||||
|
pcm_dir = tmp_path / "pcm"
|
||||||
|
windows: dict[int, bytes] = {}
|
||||||
|
for index in indices:
|
||||||
|
padded = _padded_pcm(records[index])
|
||||||
|
windows[index] = padded
|
||||||
|
_write_hex(pcm_dir / f"pcm_{index:06d}.hex", padded)
|
||||||
|
apum_hex = tmp_path / "kws_apum.hex"
|
||||||
|
_write_hex(apum_hex, import_tflite(TFLITE))
|
||||||
|
binaries = _compile_dump_tb(tmp_path)
|
||||||
|
dumps: dict[int, tuple[bytes, int]] = {}
|
||||||
|
for first, count in spans:
|
||||||
|
plusargs = [
|
||||||
|
f"+APUM_HEX={apum_hex}",
|
||||||
|
f"+PCM_DIR={pcm_dir}",
|
||||||
|
f"+WINDOW_COUNT={count}",
|
||||||
|
f"+FIRST_INDEX={first}",
|
||||||
|
"+MAX_WINDOW_CYCLES=4000000",
|
||||||
|
]
|
||||||
|
command = (
|
||||||
|
[str(binaries["verilator"]), *plusargs]
|
||||||
|
if verilator is not None
|
||||||
|
else ["vvp", str(binaries["icarus"]), *plusargs]
|
||||||
|
)
|
||||||
|
result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=3600)
|
||||||
|
features: dict[int, bytes] = {}
|
||||||
|
peaks: dict[int, int] = {}
|
||||||
|
completed = 0
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
mfcc_match = MFCC_PATTERN.match(line)
|
||||||
|
if mfcc_match:
|
||||||
|
features[int(mfcc_match.group(1))] = bytes.fromhex(mfcc_match.group(2))
|
||||||
|
continue
|
||||||
|
peak_match = PEAK_PATTERN.match(line)
|
||||||
|
if peak_match:
|
||||||
|
peaks[int(peak_match.group(1))] = int(peak_match.group(2))
|
||||||
|
continue
|
||||||
|
complete_match = COMPLETE_PATTERN.match(line)
|
||||||
|
if complete_match:
|
||||||
|
assert int(complete_match.group(1)) == count
|
||||||
|
assert int(complete_match.group(2)) == first
|
||||||
|
completed += 1
|
||||||
|
assert completed == 1, f"dump simulator output lacks the completion marker:\n{result.stdout}"
|
||||||
|
for index in range(first, first + count):
|
||||||
|
assert index in features and index in peaks, f"window {index} dump missing"
|
||||||
|
dumps[index] = (features[index], peaks[index])
|
||||||
|
assert sorted(dumps) == indices
|
||||||
|
for index in indices:
|
||||||
|
stages = bam.frontend_stages(bam.samples_from_pcm(windows[index]))
|
||||||
|
rtl_features, rtl_peak = dumps[index]
|
||||||
|
assert stages.peak == rtl_peak, (
|
||||||
|
f"window {index} peak: BAM {stages.peak} != RTL {rtl_peak}"
|
||||||
|
)
|
||||||
|
assert stages.features == rtl_features, (
|
||||||
|
f"window {index}: RTL frontend MFCC bytes are not bit-exact against the BAM"
|
||||||
|
)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""APU-P7 concurrent decode + continuous KWS zero-xrun smoke qualification."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER = ROOT / "scripts" / "run_apu_p7_concurrent_rtl.py"
|
||||||
|
TFLITE = (
|
||||||
|
ROOT
|
||||||
|
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
|
||||||
|
/ "trained_models/kws_ref_model.tflite"
|
||||||
|
)
|
||||||
|
|
||||||
|
REQUIRED_REPORT_KEYS = {
|
||||||
|
"schema_version",
|
||||||
|
"phase",
|
||||||
|
"report",
|
||||||
|
"status",
|
||||||
|
"success",
|
||||||
|
"git_revision",
|
||||||
|
"git_dirty",
|
||||||
|
"command",
|
||||||
|
"seed",
|
||||||
|
"profile",
|
||||||
|
"inputs",
|
||||||
|
"converter",
|
||||||
|
"bam",
|
||||||
|
"rtl_revision",
|
||||||
|
"verification_sources",
|
||||||
|
"simulator",
|
||||||
|
"tools",
|
||||||
|
"matrix",
|
||||||
|
"scenarios",
|
||||||
|
"aggregate_counts",
|
||||||
|
"full_gate_projection",
|
||||||
|
"artifacts",
|
||||||
|
"errors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apumc_bundle() -> Path | None:
|
||||||
|
candidates = sorted(ROOT.glob("build/*/apu/p5/apu-p5.apumc"), key=lambda path: path.stat().st_mtime)
|
||||||
|
return candidates[-1] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p7_concurrent_smoke(tmp_path: Path) -> None:
|
||||||
|
missing_tools = [tool for tool in ("iverilog", "vvp", "sv2v") if shutil.which(tool) is None]
|
||||||
|
if missing_tools:
|
||||||
|
pytest.skip(f"APU-P7 concurrent smoke requires tools: {', '.join(missing_tools)}")
|
||||||
|
if _apumc_bundle() is None:
|
||||||
|
pytest.skip("APU-P5 bundle was not built (make CONFIG=configs/ci/ihp130.mk apu-p5-bundle)")
|
||||||
|
if not TFLITE.is_file():
|
||||||
|
pytest.skip("locked P7 model was not installed")
|
||||||
|
|
||||||
|
build_dir = tmp_path / "concurrent"
|
||||||
|
report = build_dir / "concurrent.json"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(RUNNER),
|
||||||
|
"--build-dir",
|
||||||
|
str(build_dir),
|
||||||
|
"--smoke",
|
||||||
|
"--output",
|
||||||
|
str(report),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=1800,
|
||||||
|
)
|
||||||
|
assert completed.returncode == 0, (
|
||||||
|
f"concurrent smoke failed:\n{completed.stdout[-2000:]}\n{completed.stderr[-2000:]}"
|
||||||
|
)
|
||||||
|
data = json.loads(report.read_text(encoding="utf-8"))
|
||||||
|
assert REQUIRED_REPORT_KEYS <= set(data)
|
||||||
|
assert data["status"] == "passed"
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["errors"] == []
|
||||||
|
assert data["report"] == "concurrent"
|
||||||
|
assert data["phase"] == "APU-P7"
|
||||||
|
assert data["pclk_hz"] == 48_000_000
|
||||||
|
assert data["inputs"]["apum"]["matches_locked_freeze"] is True
|
||||||
|
assert len(data["scenarios"]) == len(data["matrix"]) >= 1
|
||||||
|
for scenario in data["scenarios"]:
|
||||||
|
assert scenario["status"] == "passed"
|
||||||
|
result = scenario["result"]
|
||||||
|
assert result["underrun"] == 0
|
||||||
|
assert result["rx_overrun"] == 0
|
||||||
|
assert result["kws_overrun"] == 0
|
||||||
|
assert result["scoreboard"] == "match"
|
||||||
|
assert result["wav_jobs"] >= 1
|
||||||
|
assert result["flac_jobs"] >= 1
|
||||||
|
assert result["tx_words"] == result["consumed"]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Smoke test for the APU-P7 KWS through-hardware accuracy qualification runner."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER = ROOT / "scripts/run_apu_p7_kws_rtl.py"
|
||||||
|
CACHE_INPUTS = (
|
||||||
|
ROOT / ".cache/retrosoc/sources/apu-kws-pcm",
|
||||||
|
ROOT / ".cache/retrosoc/sources/apu-kws-mfcc/datasets/kws01/y_labels.csv",
|
||||||
|
ROOT
|
||||||
|
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
|
||||||
|
/ "trained_models/kws_ref_model.tflite",
|
||||||
|
)
|
||||||
|
# Icarus needs ~7 minutes per window on this engine; keep the smoke short there.
|
||||||
|
SMOKE_WINDOWS = 4 if shutil.which("verilator") is not None else 1
|
||||||
|
REQUIRED_KEYS = (
|
||||||
|
"schema_version",
|
||||||
|
"report",
|
||||||
|
"target",
|
||||||
|
"profile",
|
||||||
|
"success",
|
||||||
|
"gate",
|
||||||
|
"seed",
|
||||||
|
"command",
|
||||||
|
"counts",
|
||||||
|
"per_class",
|
||||||
|
"incorrect_indices",
|
||||||
|
"predictions",
|
||||||
|
"inputs",
|
||||||
|
"converter",
|
||||||
|
"reference",
|
||||||
|
"rtl",
|
||||||
|
"model",
|
||||||
|
"rom",
|
||||||
|
"tools",
|
||||||
|
"simulation",
|
||||||
|
"artifacts",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p7_accuracy_runner_smoke(tmp_path: Path) -> None:
|
||||||
|
if any(shutil.which(tool) is None for tool in ("iverilog", "vvp", "sv2v")):
|
||||||
|
return
|
||||||
|
if any(not path.exists() for path in CACHE_INPUTS):
|
||||||
|
return
|
||||||
|
build_dir = tmp_path / "apu-kws"
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(RUNNER),
|
||||||
|
"--build-dir",
|
||||||
|
str(build_dir),
|
||||||
|
"--max-windows",
|
||||||
|
str(SMOKE_WINDOWS),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=1800,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
report_path = build_dir / "accuracy.json"
|
||||||
|
assert report_path.is_file()
|
||||||
|
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||||||
|
for key in REQUIRED_KEYS:
|
||||||
|
assert key in report, f"accuracy.json is missing required key {key}"
|
||||||
|
assert report["report"] == "accuracy"
|
||||||
|
assert report["target"] == "p7"
|
||||||
|
assert report["success"] is True
|
||||||
|
assert report["gate"]["applies"] is False
|
||||||
|
assert report["counts"]["windows_requested"] == SMOKE_WINDOWS
|
||||||
|
assert report["counts"]["windows_completed"] == SMOKE_WINDOWS
|
||||||
|
assert len(report["per_class"]) == 12
|
||||||
|
assert sum(item["total"] for item in report["per_class"]) == SMOKE_WINDOWS
|
||||||
|
predictions = report["predictions"]
|
||||||
|
assert [item["index"] for item in predictions] == list(range(SMOKE_WINDOWS))
|
||||||
|
assert all(0 <= item["predicted"] < 12 for item in predictions)
|
||||||
|
assert all(0 <= item["score"] <= 255 for item in predictions)
|
||||||
|
assert all(item["hit"] in (0, 1) for item in predictions)
|
||||||
|
assert report["model"]["apum_sha256"] == report["model"]["frozen_sha256"]
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Directed USB2/SDIO0/APU Gateway A contention evidence for APU-P8."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p8_gateway_a_contention(tmp_path: Path) -> None:
|
||||||
|
iverilog = shutil.which("iverilog")
|
||||||
|
vvp = shutil.which("vvp")
|
||||||
|
sv2v = shutil.which("sv2v")
|
||||||
|
if iverilog is None or vvp is None or sv2v is None:
|
||||||
|
pytest.skip("iverilog, vvp, or sv2v is not installed")
|
||||||
|
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
source_list = tmp_path / "apu_p8_gateway_contention.fl"
|
||||||
|
source_list.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"+define+SV_ASSRT_DISABLE",
|
||||||
|
f"+incdir+{common}",
|
||||||
|
f"+incdir+{common / 'interface'}",
|
||||||
|
str(common / "interface/axi4_if.sv"),
|
||||||
|
str(common / "utils/register.sv"),
|
||||||
|
str(common / "stream/round_robin_arbiter.sv"),
|
||||||
|
str(ROOT / "rtl/mini/top/hp_axi4_mux3.sv"),
|
||||||
|
str(ROOT / "tests/rtl/apu_p8_gateway_contention_tb.sv"),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
converted = tmp_path / "apu_p8_gateway_contention_tb.v"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||||
|
"-f",
|
||||||
|
str(source_list),
|
||||||
|
"--output",
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
simulation = tmp_path / "apu_p8_gateway_contention_tb"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
iverilog,
|
||||||
|
"-g2012",
|
||||||
|
"-s",
|
||||||
|
"apu_p8_gateway_contention_tb",
|
||||||
|
"-o",
|
||||||
|
str(simulation),
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
result = subprocess.run([vvp, str(simulation)], check=True, capture_output=True, text=True)
|
||||||
|
assert "APU-P8 Gateway A contention passed" in result.stdout
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""APU-P8 regression: quiesced MICROCODE_LOAD/MODEL_LOAD DMA admission."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
KWS_MODEL = (
|
||||||
|
ROOT
|
||||||
|
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
|
||||||
|
/ "trained_models/kws_ref_model.tflite"
|
||||||
|
)
|
||||||
|
|
||||||
|
PRODUCTION_SOURCES = (
|
||||||
|
"apu_microcode_pkg.sv",
|
||||||
|
"apu_dma.sv",
|
||||||
|
"apu_ring_scheduler.sv",
|
||||||
|
"apu_stream_router.sv",
|
||||||
|
"apu_control_store.sv",
|
||||||
|
"apu_microcode_loader.sv",
|
||||||
|
"apu_local_sram.sv",
|
||||||
|
"apu_kws_engine.sv",
|
||||||
|
"apu_kws_sram_client.sv",
|
||||||
|
"apu_kws_model_loader.sv",
|
||||||
|
"apu_bitstream_engine.sv",
|
||||||
|
"apu_entropy_engine.sv",
|
||||||
|
"apu_reconstruction_engine.sv",
|
||||||
|
"apu_transform_engine.sv",
|
||||||
|
"apu_resampler.sv",
|
||||||
|
"apu_kernel_engine.sv",
|
||||||
|
"apu_primitive_dispatcher.sv",
|
||||||
|
"apu_codec_sequencer.sv",
|
||||||
|
"apu_codec_transport.sv",
|
||||||
|
"apu_codec_controller.sv",
|
||||||
|
"apu_reg.sv",
|
||||||
|
"apb4_apu.sv",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apumc_bundle() -> Path | None:
|
||||||
|
candidates = sorted(
|
||||||
|
ROOT.glob("build/*/apu/p5/apu-p5.apumc"), key=lambda path: path.stat().st_mtime
|
||||||
|
)
|
||||||
|
return candidates[-1] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def _hex_words(path: Path, payload: bytes, words: int) -> None:
|
||||||
|
lines = []
|
||||||
|
for index in range(words):
|
||||||
|
chunk = payload[index * 4 : index * 4 + 4]
|
||||||
|
lines.append(f"{int.from_bytes(chunk.ljust(4, bytes(1)), 'little'):08x}\n")
|
||||||
|
path.write_text("".join(lines), encoding="ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_p8_quiesced_loader_admission(tmp_path: Path) -> None:
|
||||||
|
iverilog = shutil.which("iverilog")
|
||||||
|
vvp = shutil.which("vvp")
|
||||||
|
sv2v = shutil.which("sv2v")
|
||||||
|
if iverilog is None or vvp is None or sv2v is None:
|
||||||
|
pytest.skip("APU-P8 quiesced loader admission requires iverilog, vvp, and sv2v")
|
||||||
|
bundle = _apumc_bundle()
|
||||||
|
if bundle is None:
|
||||||
|
pytest.skip("APU-P5 bundle was not built (make CONFIG=configs/ci/ihp130.mk apu-p5-bundle)")
|
||||||
|
if not KWS_MODEL.is_file():
|
||||||
|
pytest.skip("locked P7 model was not installed")
|
||||||
|
|
||||||
|
from apu_kws_convert import import_tflite
|
||||||
|
|
||||||
|
apumc_hex = tmp_path / "apu-p5.hex"
|
||||||
|
_hex_words(apumc_hex, bundle.read_bytes(), 16384)
|
||||||
|
apum_hex = tmp_path / "kws-apum.hex"
|
||||||
|
_hex_words(apum_hex, import_tflite(KWS_MODEL), 8192)
|
||||||
|
|
||||||
|
common = ROOT / "rtl/managed/clusterip/common/rtl"
|
||||||
|
multimedia = ROOT / "rtl/ip/multimedia"
|
||||||
|
source_list = tmp_path / "apu_p8_quiesce_load.fl"
|
||||||
|
source_list.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"+define+SV_ASSRT_DISABLE",
|
||||||
|
f"+incdir+{common}",
|
||||||
|
f"+incdir+{common / 'interface'}",
|
||||||
|
f"+incdir+{multimedia}",
|
||||||
|
str(common / "interface/apb4_if.sv"),
|
||||||
|
str(common / "interface/axi4_if.sv"),
|
||||||
|
str(common / "interface/axi4_stream_if.sv"),
|
||||||
|
str(common / "utils/register.sv"),
|
||||||
|
str(common / "utils/fifo.sv"),
|
||||||
|
str(ROOT / "rtl/tech/tc_sram.sv"),
|
||||||
|
str(ROOT / "rtl/ip/peripheral/dma_axi4_master.sv"),
|
||||||
|
*(str(multimedia / name) for name in PRODUCTION_SOURCES),
|
||||||
|
str(ROOT / "tests/rtl/apu_p8_quiesce_load_tb.sv"),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
converted = tmp_path / "apu_p8_quiesce_load.v"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
|
||||||
|
"-f",
|
||||||
|
str(source_list),
|
||||||
|
"--output",
|
||||||
|
str(converted),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
simulation = tmp_path / "apu_p8_quiesce_load"
|
||||||
|
subprocess.run(
|
||||||
|
[iverilog, "-g2012", "-s", "apu_p8_quiesce_load_tb", "-o", str(simulation), str(converted)],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
vvp,
|
||||||
|
str(simulation),
|
||||||
|
f"+APUMC_HEX={apumc_hex}",
|
||||||
|
f"+APUM_HEX={apum_hex}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert "P8_QUIESCE_MC" in result.stdout
|
||||||
|
assert "P8_QUIESCE_MODEL" in result.stdout
|
||||||
|
assert "APU-P8 quiesced loader admission test passed" in result.stdout
|
||||||
@@ -11,6 +11,11 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
sys.path.insert(0, str(ROOT / "scripts"))
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
from apu_isa import abi_manifest # noqa: E402
|
from apu_isa import abi_manifest # noqa: E402
|
||||||
|
from apu_abi_digest import ( # noqa: E402
|
||||||
|
c_implemented_digest,
|
||||||
|
compute_abi_digest,
|
||||||
|
rtl_implemented_digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
RTL_DEFINE = ROOT / "rtl/ip/multimedia/apu_define.svh"
|
RTL_DEFINE = ROOT / "rtl/ip/multimedia/apu_define.svh"
|
||||||
@@ -143,3 +148,10 @@ def test_apu_p4_tool_isa_matches_handwritten_rtl_and_c() -> None:
|
|||||||
if name != "reserved":
|
if name != "reserved":
|
||||||
exact[f"MC_TRAP_{trap_names[name]}"] = value
|
exact[f"MC_TRAP_{trap_names[name]}"] = value
|
||||||
assert {name: rtl[name] for name in exact} == exact
|
assert {name: rtl[name] for name in exact} == exact
|
||||||
|
|
||||||
|
|
||||||
|
def test_apu_abi_digest_matches_computed_value() -> None:
|
||||||
|
computed = compute_abi_digest()
|
||||||
|
assert computed != 0
|
||||||
|
assert rtl_implemented_digest() == computed
|
||||||
|
assert c_implemented_digest() == computed
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""APU-P8 HP post-fault visibility reproduction at the LP interconnect level."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_axi4_interconnect_hp_postfault_visibility(tmp_path: Path) -> None:
|
||||||
|
verilator = shutil.which("verilator")
|
||||||
|
if verilator is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
memory_map = tmp_path / "memory_map"
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(ROOT / "rtl/mini/address_map/generate_memory_map.py"),
|
||||||
|
"--map",
|
||||||
|
str(ROOT / "rtl/mini/address_map/memory_map.json"),
|
||||||
|
"--output-dir",
|
||||||
|
str(memory_map),
|
||||||
|
"--have-sram-if",
|
||||||
|
"NO",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
output = tmp_path / "axi4_interconnect_hp_postfault_tb"
|
||||||
|
ccache_tmp = tmp_path / "ccache"
|
||||||
|
ccache_tmp.mkdir()
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
verilator,
|
||||||
|
"--binary",
|
||||||
|
"--timing",
|
||||||
|
"-Wno-fatal",
|
||||||
|
"--top-module",
|
||||||
|
"axi4_interconnect_hp_postfault_tb",
|
||||||
|
"-DMINI_PRODUCT",
|
||||||
|
"-I" + str(memory_map / "rtl"),
|
||||||
|
"-I" + str(ROOT / "rtl/mini/top"),
|
||||||
|
"-I" + str(ROOT / "rtl/managed/clusterip/common/rtl"),
|
||||||
|
"-I" + str(ROOT / "rtl/managed/clusterip/common/rtl/interface"),
|
||||||
|
str(ROOT / "rtl/managed/clusterip/common/rtl/interface/axi4_if.sv"),
|
||||||
|
str(ROOT / "rtl/managed/clusterip/common/rtl/utils/register.sv"),
|
||||||
|
str(ROOT / "rtl/managed/clusterip/common/rtl/stream/round_robin_arbiter.sv"),
|
||||||
|
str(ROOT / "rtl/mini/top/axi4_interconnect.sv"),
|
||||||
|
str(ROOT / "rtl/mini/top/axi4_error_slave.sv"),
|
||||||
|
str(ROOT / "tests/rtl/axi4_interconnect_hp_postfault_tb.sv"),
|
||||||
|
"-Mdir",
|
||||||
|
str(tmp_path / "obj"),
|
||||||
|
"-o",
|
||||||
|
str(output),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"CCACHE_DIR": str(ccache_tmp),
|
||||||
|
"CCACHE_TEMPDIR": str(ccache_tmp),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = subprocess.run([output], check=True, text=True, capture_output=True)
|
||||||
|
assert "AXI4 interconnect HP post-fault visibility test passed" in result.stdout
|
||||||
@@ -27,13 +27,14 @@ def reference():
|
|||||||
return collect_registers()
|
return collect_registers()
|
||||||
|
|
||||||
|
|
||||||
def test_current_apu_interface_advertises_the_expanded_p5_profile(reference):
|
def test_current_apu_interface_declares_p5_default_and_p7_release(reference):
|
||||||
registers = {r["name"]: r for r in reference["apu"]["registers"]}
|
registers = {r["name"]: r for r in reference["apu"]["registers"]}
|
||||||
constants = (ROOT / "crt/include/retrosoc/hal/apu_regs.h").read_text(
|
constants = (ROOT / "crt/include/retrosoc/hal/apu_regs.h").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
assert re.search(r"RS_APU_IP_VERSION_VALUE\s+UINT32_C\(0x00010001\)", constants)
|
assert re.search(r"RS_APU_IP_VERSION_VALUE\s+UINT32_C\(0x00010001\)", constants)
|
||||||
assert re.search(r"RS_APU_CAPABILITY0_IMPLEMENTED\s+UINT32_C\(0x000001BD\)", constants)
|
assert re.search(r"RS_APU_CAPABILITY0_P5_IMPLEMENTED\s+UINT32_C\(0x000001BD\)", constants)
|
||||||
|
assert re.search(r"RS_APU_CAPABILITY0_P7_IMPLEMENTED\s+UINT32_C\(0x000001FD\)", constants)
|
||||||
assert re.search(r"RS_APU_CAPABILITY1_IMPLEMENTED\s+UINT32_C\(0x01827020\)", constants)
|
assert re.search(r"RS_APU_CAPABILITY1_IMPLEMENTED\s+UINT32_C\(0x01827020\)", constants)
|
||||||
assert "V1.1" in registers["IP_VERSION"]["description"]
|
assert "V1.1" in registers["IP_VERSION"]["description"]
|
||||||
assert "32 KiB" in registers["CAPABILITY1"]["description"]
|
assert "32 KiB" in registers["CAPABILITY1"]["description"]
|
||||||
|
|||||||
Reference in new issue
Block a user