diff --git a/dv/uvm/core_ibex/Makefile b/dv/uvm/core_ibex/Makefile index 896288ac..5c3e0640 100644 --- a/dv/uvm/core_ibex/Makefile +++ b/dv/uvm/core_ibex/Makefile @@ -5,7 +5,22 @@ DV_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) GEN_DIR := $(realpath ${DV_DIR}/../../../vendor/google_riscv-dv) TOOLCHAIN := ${RISCV_TOOLCHAIN} -OUT := "${DV_DIR}/out" + +# Seed for instruction generator and RTL simulation +# +# By default, SEED is set to a different value on each run by picking a random +# value in the Makefile. For overnight testing, a sensible seed might be +# something like the output of "date +%y%m%d". For regression testing, you'll +# need to make sure that a the seed for a failed test "sticks" (so we don't +# start passing again without fixing the bug). +SEED := $(shell echo $$RANDOM) + +# This is the top-level output directory. Everything we generate goes in +# here. Most generated stuff actually goes in $(OUT)/seed-$(SEED), which allows +# us to run multiple times without deleting existing results. +OUT := ${DV_DIR}/out +OUT-SEED := $(OUT)/seed-$(SEED) + # Run time options for the instruction generator GEN_OPTS := # Compile time options for ibex RTL simulation @@ -26,9 +41,7 @@ ISS_OPTS := ISA := rv32imc # Test name (default: full regression) TEST := all -# Seed for instruction generator and RTL simulation TESTLIST := ${DV_DIR}/riscv_dv_extension/testlist.yaml -SEED := -1 # Verbose logging VERBOSE := # Number of iterations for each test, assign a non-zero value to override the @@ -68,29 +81,28 @@ endif SHELL=/bin/bash -export PRJ_DIR:= $(realpath ${DV_DIR}/../../../../) +export PRJ_DIR:= $(realpath ${DV_DIR}/../../../..) -.PHONY: rtl_sim clean gcc_compile iss_sim +all: sim -all: clean gen gcc_compile iss_sim compile rtl_sim post_compare +instr: iss_sim -instr: gen gcc_compile iss_sim - -sim: compile rtl_sim post_compare +sim: post_compare +.PHONY: clean clean: rm -rf ${OUT} # Common options for all targets -COMMON_OPTS:=--seed=${SEED} \ +COMMON_OPTS := $(if $(call equal,$(VERBOSE),1),--verbose,) + +# Options for all targets that depend on the tests we're running. +TEST_OPTS := $(COMMON_OPTS) \ + --seed=${SEED} \ --test"=${TEST}" \ --testlist=${TESTLIST} \ --iterations=${ITERATIONS} -ifeq ($(VERBOSE), 1) - COMMON_OPTS+=--verbose -endif - # Options used for privileged CSR test generation CSR_OPTS=--csr_yaml=${CSR_FILE} \ --isa="${ISA}" \ @@ -100,45 +112,208 @@ RISCV_DV_OPTS=--custom_target=${DV_DIR}/riscv_dv_extension \ --isa="${ISA}" \ --mabi=ilp32 \ +# To avoid cluttering the output directory with stamp files, we place them in +# $(metadata). +metadata := $(OUT-SEED)/.metadata + +# This is a list of directories that are automatically generated by some +# targets. To ensure the directory has been built, add a order-only dependency +# (with the pipe symbol before it) on the directory name and add the directory +# to this list. +gen-dirs := $(OUT) $(OUT-SEED) $(metadata) $(OUT)/rtl_sim + +$(gen-dirs): %: + mkdir -p $@ + +############################################################################### +# Utility functions. +# +# If VS is a list of variable names, P is a path and X is a string, then $(call +# dump-vars,P,X,VS) will expand to a list of 'file' commands that write each +# variable to P in Makefile syntax, but with "last-X-" prepended. At the start +# of the file, we also define last-X-vars-loaded to 1. You can use this to +# check whether there was a dump file at all. +# +# Note that this doesn't work by expanding to a command. Instead, *evaluating* +# dump-vars causes the variables to be dumped. +dump-var = $(file >>$(1),last-$(2)-$(3) := $($(3))) +dump-vars = $(file >$(1),last-$(2)-vars-loaded := .) \ + $(foreach name,$(3),$(call dump-var,$(1),$(2),$(name))) + +# equal checks whether two strings are equal, evaluating to '.' if they are and +# '' otherwise. +both-empty = $(if $(1),,$(if $(2),,.)) +find-find = $(if $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))),.,) +equal = $(or $(call both-empty,$(1),$(2)),$(call find-find,$(1),$(2))) + +# var-differs is used to check whether a variable has changed since it was +# dumped. If it has changed, the function evaluates to '.' (with some +# whitespace) and prints a message to the console; if not, it evaluates to ''. +# +# Call it as $(call var-differs,X,TGT,V). +var-differs = \ + $(if $(call equal,$(strip $($(3))),$(strip $(last-$(1)-$(3)))),,\ + .$(info Repeating $(2) because variable $(3) has changed value.)) + +# vars-differ is used to check whether several variables have the same value as +# they had when they were dumped. If we haven't loaded the dumpfile, it +# silently evaluates to '!'. Otherwise, if all the variables match, it +# evaluates to '.'. If not, it evaluates to '.' and prints some messages to the +# console explaining why a rebuild is happening. +# +# Call it as $(call vars-differ,X,TGT,VS). +vars-differ-lst = $(foreach v,$(3),$(call var-differs,$(1),$(2),$(v))) +vars-differ-sp = \ + $(if $(last-$(1)-vars-loaded),\ + $(if $(strip $(call vars-differ-lst,$(1),$(2),$(3))),.,),\ + !) +vars-differ = $(strip $(call vars-differ-sp,$(1),$(2),$(3))) + +# A phony target which can be used to force recompilation. +.PHONY: FORCE +FORCE: + +# vars-prereq is empty if every variable in VS matches the last run (loaded +# with tag X), otherwise it is set to FORCE (which will force a recompile and +# might print a message to the console explaining why we're rebuilding TGT). +# +# Call it as $(call vars-prereq,X,TGT,VS) +vars-prereq = $(if $(call vars-differ,$(1),$(2),$(3)),FORCE,) + +############################################################################### # Generate random instructions -.SILENT gen: - mkdir -p ${OUT} - python3 ${GEN_DIR}/run.py \ - --output=${OUT}/instr_gen ${GEN_OPTS} \ +# +# This depends on the vendored in code in $(GEN_DIR). It also depends on the +# values of some variables (we want to regenerate things if, for example, the +# simulator changes). Since we're writing out to $(OUT-SEED), we don't have to +# depend on the value of SEED. However, we do have to make sure that the +# variables whose names are listed in $(gen-var-deps) haven't changed. +# +# To do this variable tracking, we dump each of the variables to a Makefile +# fragment and try to load it up the next time around. +gen-var-deps := GEN_OPTS SIMULATOR RISCV_DV_OPTS CSR_OPTS \ + SIGNATURE_ADDR PMP_REGIONS PMP_GRANULARITY TEST_OPTS + +# Load up the generation stage's saved variable values. If this fails, that's +# no problem: we'll assume that the previous run either doesn't exist or +# something went wrong. +-include $(metadata)/gen-vars.mk + +# gen-vars-prereq is empty if every variable in gen-var-deps matches the last run, +# otherwise it is set to FORCE (which will force a recompile). Note that we +# define it with '=', not ':=', so we don't evaluate it if we're not trying to +# run the gen target. +gen-vars-prereq = \ + $(call vars-prereq,gen,building instruction generator,$(gen-var-deps)) + +# A variable containing a file list for the riscv-dv vendored-in module. +# Depending on these files gives a safe over-approximation that will ensure we +# rebuild things if that module changes. +# +# Note that this is defined with ":=". As a result, we'll always run the find +# command exactly once. Wasteful if we're trying to make clean, but much better +# than running it for every target otherwise. +risc-dv-files := $(shell find $(GEN_DIR) -type f) + +# This actually runs the instruction generator. Note that the rule depends on +# the (phony) FORCE target if any variables have changed. If the rule actually +# runs, it starts by deleting any existing contents of $(OUT-SEED)/instr_gen. +$(metadata)/instr_gen.gen.stamp: \ + $(gen-vars-prereq) $(risc-dv-files) | $(metadata) + @rm -rf $(OUT-SEED)/instr_gen + @python3 ${GEN_DIR}/run.py \ + --output=$(OUT-SEED)/instr_gen ${GEN_OPTS} \ --steps=gen \ --gen_timeout=${TIMEOUT} \ --lsf_cmd="${LSF_CMD}" \ --simulator="${SIMULATOR}" \ ${RISCV_DV_OPTS} \ - ${COMMON_OPTS} \ + ${TEST_OPTS} \ ${CSR_OPTS} \ --sim_opts="+uvm_set_inst_override=riscv_asm_program_gen,ibex_asm_program_gen,"uvm_test_top.asm_gen" \ +signature_addr=${SIGNATURE_ADDR} +pmp_num_regions=${PMP_REGIONS} \ - +pmp_granularity=${PMP_GRANULARITY}"; + +pmp_granularity=${PMP_GRANULARITY}" + $(call dump-vars,$(metadata)/gen-vars.mk,gen,$(gen-var-deps)) + @touch $@ -# Compile the generated assmebly programs to ELF/BIN -gcc_compile: - python3 ${GEN_DIR}/run.py \ - --o=${OUT}/instr_gen ${GEN_OPTS} \ +.PHONY: gen +gen: $(metadata)/instr_gen.gen.stamp + +############################################################################### +# Compile the generated assembly programs +# +# We don't explicitly track dependencies on the RISCV toolchain, so this +# doesn't depend on anything more than the instr_gen stage did. +$(metadata)/instr_gen.compile.stamp: $(metadata)/instr_gen.gen.stamp + @python3 ${GEN_DIR}/run.py \ + --o=$(OUT-SEED)/instr_gen ${GEN_OPTS} \ --steps=gcc_compile \ - ${COMMON_OPTS} \ + ${TEST_OPTS} \ --gcc_opts=-mno-strict-align \ - ${RISCV_DV_OPTS} \ + ${RISCV_DV_OPTS} && \ + touch $@ -# ISS simulation -iss_sim: - python3 ${GEN_DIR}/run.py \ - --o=${OUT}/instr_gen ${GEN_OPTS} \ +.PHONY: gcc_compile +gcc_compile: $(metadata)/instr_gen.compile.stamp + +############################################################################### +# Run the instruction set simulator +# +# This (obviously) depends on having compiled the generated programs, so we +# don't have to worry about variables that affect the 'gen' stage. However, the +# ISS and ISS_OPTS variables do affect the output, so we need to dump them. See +# the 'gen' stage for more verbose explanations of how this works. +iss-var-deps := ISS ISS_OPTS +-include $(metadata)/iss-vars.mk +iss-vars-prereq = $(call vars-prereq,iss,running ISS,$(iss-var-deps)) + +$(metadata)/instr_gen.iss.stamp: \ + $(iss-vars-prereq) $(metadata)/instr_gen.compile.stamp + @python3 ${GEN_DIR}/run.py \ + --o=$(OUT-SEED)/instr_gen ${GEN_OPTS} \ --steps=iss_sim \ - ${COMMON_OPTS} \ + ${TEST_OPTS} \ --iss="${ISS}" \ --iss_opts="${ISS_OPTS}" \ - ${RISCV_DV_OPTS} \ + ${RISCV_DV_OPTS} + $(call dump-vars,$(metadata)/iss-vars.mk,iss,$(iss-var-deps)) + @touch $@ +.PHONY: iss_sim +iss_sim: $(metadata)/instr_gen.iss.stamp + + +############################################################################### # Compile ibex core TB -compile: - mkdir -p ${OUT}/rtl_sim; - python3 ./sim.py \ +# +# Note that (unlike everything else) this doesn't depend on the seed: the DUT +# doesn't depend on which test we're running! +# +# It does, however, depend on various variables. These are listed in +# compile-var-deps. See the 'gen' stage for more verbose explanations of how +# the variable dumping works. +# +# The compiled ibex testbench (obviously!) also depends on the design and the +# DV code. The clever way of doing this would be to look at a dependency +# listing generated by the simulator as a side-effect of doing the compile (a +# bit like using the -M flags with a C compiler). Unfortunately, that doesn't +# look like it's particularly easy, so we'll just depend on every .v, .sv or +# .svh file in the dv or rtl directories. Note that this variable is set with +# '=', rather than ':='. This means that we don't bother running the find +# commands unless we need the compiled testbench. +all-verilog = \ + $(shell find ../../../rtl -name '*.v' -o -name '*.sv' -o -name '*.svh') \ + $(shell find ../.. -name '*.v' -o -name '*.sv' -o -name '*.svh') + +compile-var-deps := COMMON_OPTS SIMULATOR COV WAVES COMPILE_OPTS +-include $(OUT)/rtl_sim/.compile-vars.mk +compile-vars-prereq = $(call vars-prereq,comp,compiling TB,$(compile-var-deps)) + +$(call dump-vars-match,$(compile-var-deps),comp) + +$(OUT)/rtl_sim/.compile.stamp: \ + $(compile-vars-prereq) $(all-verilog) $(risc-dv-files) | $(OUT)/rtl_sim + @python3 ./sim.py \ --o=${OUT} \ --riscv_dv_root=${GEN_DIR} \ --steps=compile \ @@ -146,34 +321,67 @@ compile: --simulator="${SIMULATOR}" \ --en_cov=${COV} \ --en_wave=${WAVES} \ - --cmp_opts="${COMPILE_OPTS}" \ + --cmp_opts="${COMPILE_OPTS}" + $(call dump-vars,$(OUT)/rtl_sim/.compile-vars.mk,comp,$(compile-var-deps)) + @touch $@ -# Run ibex RTL simulation with random instructions -rtl_sim: - mkdir -p ${OUT}/rtl_sim - python3 ./sim.py \ - --o=${OUT} \ +.PHONY: compile +compile: $(OUT)/rtl_sim/.compile.stamp + +############################################################################### +# Run ibex RTL simulation with generated programs +# +# Because we compile a TB once rather than for each seed, we have to copy in +# that directory before we start. We make this step (rather than actually +# running the test) dependent on having the right variables. That way, we'll +# correctly delete the sim directory and re-copy it if necessary. +# +# Note that the variables we depend on are gen-vars-prereq. We also depend on +# COV and WAVES, but these dependencies will come for free from the dependency +# on the compiled TB. +$(metadata)/rtl_sim.compile.stamp: \ + $(gen-vars-prereq) $(risc-dv-files) $(OUT)/rtl_sim/.compile.stamp + rm -rf $(OUT-SEED)/rtl_sim + cp -r $(OUT)/rtl_sim $(OUT-SEED) + @touch $@ + +# This rule actually runs the simulation. It depends on the copied-in testbench +# and also on us having already compiled the test programs. +$(metadata)/rtl_sim.run.stamp: \ + $(metadata)/rtl_sim.compile.stamp $(metadata)/instr_gen.compile.stamp + @python3 ./sim.py \ + --o=$(OUT-SEED) \ --riscv_dv_root=${GEN_DIR} \ --steps=sim \ - ${COMMON_OPTS} \ + ${TEST_OPTS} \ --simulator="${SIMULATOR}" \ --en_cov ${COV} \ --en_wave ${WAVES} \ --lsf_cmd="${LSF_CMD}" \ --sim_opts="+signature_addr=${SIGNATURE_ADDR}" \ ${SIM_OPTS} + @touch $@ -# Compare the regression result between ISS and RTL sim -post_compare: - rm -rf ${OUT}/regr.log - python3 ./sim.py \ - --o=${OUT} \ +.PHONY: rtl_sim +rtl_sim: $(metadata)/rtl_sim.run.stamp + +############################################################################### +# Compare ISS and RTL sim results +$(OUT-SEED)/regr.log: \ + $(metadata)/instr_gen.iss.stamp \ + $(metadata)/rtl_sim.run.stamp + @rm -f $@ + @python3 ./sim.py \ + --o=$(OUT-SEED) \ --steps=compare \ - ${COMMON_OPTS} \ + ${TEST_OPTS} \ --simulator="${SIMULATOR}" \ --iss="${ISS}" +.PHONY: post_compare +post_compare: $(OUT-SEED)/regr.log +############################################################################### # Generate functional coverage fcov: python3 ${GEN_DIR}/cov.py \ @@ -190,5 +398,5 @@ cov_vcs: cov_ius: if [ ! -d "${OUT}/rtl_sim/cov_work/scope/merged_cov" ]; \ then imc -execcmd "merge -out ${OUT}/rtl_sim/cov_work/scope/merged_cov ${OUT}/rtl_sim/cov_work/scope/test_*"; \ - fi + fi imc -load ${OUT}/rtl_sim/cov_work/scope/merged_cov &