feat: add RTL linting support with Verilator integration

- Introduced RTL linting capabilities using Verilator in the Makefile.
- Added RTL_LINT_FLAGS for specifying linting options and created targets for linting.
- Implemented a new script to analyze RTL lint warnings and check against a baseline.
- Updated regression scripts to include RTL linting in the command sets.
- Enhanced test coverage for RTL linting functionality, ensuring proper warning handling.
- Cleaned up build directories to include RTL lint outputs.
This commit is contained in:
maksyuki committed 2026-07-25 09:14:29 +08:00
1 parent 65d974395d
commit 6f0e053d58
11 files changed
+9288 -25

No files matched your search

+2 -1
View File
@@ -171,7 +171,7 @@ endif
regress-smoke regress-pr regress-nightly sim-asm format format-check sw-format sw-format-check mk-format \
mk-format-check rtl-format rtl-format-check sw-policy-check sw-host-test \
pin-map check-pin-map soc-topology check-soc-topology user-extensions check-user-extensions \
check-clock-reset-domains tech-cell-test
check-clock-reset-domains tech-cell-test rtl-lint check-rtl-lint
.NOTPARALLEL: setup
help:
@@ -195,6 +195,7 @@ help:
' user-extensions generate the selected scalar user-extension bindings' \
' check-user-extensions validate the canonical user-extension map' \
' check-clock-reset-domains validate the root clock/reset and CDC inventory' \
' rtl-lint | check-rtl-lint run/check strict Verilator RTL lint warnings' \
' tech-cell-test test GF180/SKY130 technology IO and clock wrappers' \
' check-warnings | metrics analyze flow logs and reports' \
' check-metrics apply the committed metrics policy' \
+3 -1
View File
@@ -123,6 +123,7 @@ make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG \
| Yosys synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SYNTH=YOSYS synth` |
| Icarus netlist simulation after synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG netsim` |
| OpenSTA core timing analysis after synthesis | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk STA=OPENSTA sta` |
| Strict Verilator RTL lint | `make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=VERILATOR HAVE_SVA=YES check-rtl-lint` |
| IHP130 fast smoke suite | `make regress-smoke` |
| Pull-request regression suite | `make regress-pr` |
| Nightly regression suite | `make regress-nightly` |
@@ -131,7 +132,8 @@ make CONFIG=configs/ci/hazard3-rv32im-ihp130.mk SIMU=IVERILOG \
| Script and policy checks | `make sw-policy-check sw-host-test` |
`make regress-smoke` builds the IHP130 firmware, compiles the Verilator SVA
configuration, and runs the Icarus assembly self-test. It omits synthesis,
configuration, and runs the Icarus assembly self-test. It runs strict RTL lint
before those flows and omits synthesis,
timing, and netlist simulation for fast feedback. `make regress-pr` runs the
supported IHP130, GF180, and SKY130 PR matrices in sequence, including
slow-corner OpenSTA core timing analysis for each PDK.
+4 -4
View File
@@ -4,10 +4,10 @@
| Tier | Profile | Automated coverage |
| --- | --- | --- |
| Smoke | `configs/ci/hazard3-rv32im-ihp130.mk` | firmware, Verilator SVA compilation, Icarus assembly self-test |
| Pull request | `configs/ci/hazard3-rv32im-ihp130.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Pull request | `configs/ci/hazard3-rv32im-gf180.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Pull request | `configs/ci/hazard3-rv32im-sky130.mk` | firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Smoke | `configs/ci/hazard3-rv32im-ihp130.mk` | strict Verilator RTL lint, firmware, Verilator SVA compilation, Icarus assembly self-test |
| Pull request | `configs/ci/hazard3-rv32im-ihp130.mk` | strict Verilator RTL lint, firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Pull request | `configs/ci/hazard3-rv32im-gf180.mk` | strict Verilator RTL lint, firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Pull request | `configs/ci/hazard3-rv32im-sky130.mk` | strict Verilator RTL lint, firmware, Verilator, Icarus, Yosys, netlist Icarus, OpenSTA |
| Pull request | `configs/ci/mdd-rv32im-ihp130.mk` | firmware and Verilator |
| Nightly | `configs/nightly/picorv32-rv32im-ihp130.mk` | firmware, Verilator, and Icarus |
| Cluster | `configs/cluster/hazard3-ics55.mk` | configuration only; run with site PDK/tool access |
+4
View File
@@ -3,5 +3,9 @@
Baselines are scoped by committed profile and tool. Each entry stores a normalized warning signature,
its accepted count, and one example. Path roots and source line numbers are removed before hashing.
`rtl-lint.json` is independent from `verilator.json`: it contains warnings from the strict
`rtl-lint` elaboration, while `verilator.json` contains warnings emitted by emulator generation
and simulation.
Do not hand-edit signatures. Regenerate only from a successful flow using
`scripts/analyze_warnings.py baseline`, review the full diff, and run `make check-warnings`.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+28 -3
View File
@@ -1,4 +1,5 @@
BUILD_DIR := $(SIM_BUILD_ROOT)
RTL_LINT_DIR := $(VARIANT_ROOT)/lint/verilator
SOC_CSRC_HOME += $(RTL_PATH)/dv/verilator/csrc
SOC_CSRC_LIB_HOME += $(RTL_PATH)/dv/verilator/csrc
SOC_CXXFILES += $(sort $(wildcard $(SOC_CSRC_HOME)/*.cpp))
@@ -27,10 +28,16 @@ VERILATOR_FLAGS += -o $(BUILD_DIR)/emu
VERILATOR_FLAGS += -Mdir $(SOC_COMPILE_HOME)
VERILATOR_FLAGS += $(SOC_VSRC_INCLPATH) $(SOC_CXXFILES) $(SOC_VXXFILES)
RTL_LINT_FLAGS := --lint-only --no-timing --top-module $(SOC_VSRC_TOP)
RTL_LINT_FLAGS += --assert --Wall --timescale "1ns/1ns" -Wno-fatal
RTL_LINT_FLAGS += $(SOC_VSRC_INCLPATH) $(SOC_VXXFILES)
SOC_SIM_TIME ?= 180
VERILATOR_STAMP := $(BUILD_DIR)/verilate.stamp
VERILATOR_DEPFILE := $(BUILD_DIR)/verilate.d
VERILATOR_EMU := $(BUILD_DIR)/emu
RTL_LINT_STAMP := $(RTL_LINT_DIR)/rtl-lint.stamp
RTL_LINT_DEPFILE := $(RTL_LINT_DIR)/rtl-lint.d
VERILATOR_EXTRA_SOURCES := $(RTL_PATH)/dv/model/ESP_PSRAM64H.sv \
$(RTL_PATH)/dv/verilator/rtl/flash_read_binder.sv \
$(RTL_PATH)/dv/verilator/rtl/QSPIFlash.sv \
@@ -38,7 +45,7 @@ VERILATOR_EXTRA_SOURCES := $(RTL_PATH)/dv/model/ESP_PSRAM64H.sv \
$(SOC_CXXFILES) $(wildcard $(SOC_CSRC_HOME)/*.h) \
$(wildcard $(SOC_CSRC_HOME)/*.hpp)
-include $(VERILATOR_DEPFILE)
-include $(VERILATOR_DEPFILE) $(RTL_LINT_DEPFILE)
CCACHE := $(shell command -v ccache 2>/dev/null)
ifneq ($(CCACHE),)
@@ -64,7 +71,24 @@ $(VERILATOR_EMU): $(VERILATOR_STAMP)
-- make -j$(VERILATOR_JOBS) VM_PARALLEL_BUILDS=1 OPT_FAST=-O3 -C $(SOC_COMPILE_HOME) \
-f V$(SOC_VSRC_TOP).mk
lint: $(VERILATOR_STAMP)
$(RTL_LINT_STAMP): $(MPW_VARIANT_STAMP) $(FILELIST_STAMP) $(VERILATOR_EXTRA_SOURCES)
@mkdir -p $(RTL_LINT_DIR)
python3 $(ROOT_PATH)/scripts/run_flow.py --tool rtl-lint \
--log $(RTL_LINT_DIR)/lint.log --result $(RTL_LINT_DIR)/result-rtl-lint.json \
-- $(VERILATOR) $(RTL_LINT_FLAGS)
python3 $(RTL_PATH)/script/filelist_deps.py $(RTL_FLIST) \
$(foreach source,$(VERILATOR_EXTRA_SOURCES),--extra $(source)) \
--target $@ --output $(RTL_LINT_DEPFILE)
@touch $@
rtl-lint: $(RTL_LINT_STAMP) | manifest
check-rtl-lint: rtl-lint
python3 $(ROOT_PATH)/scripts/analyze_warnings.py check --root $(ROOT_PATH) \
--profile $(PROFILE_NAME) --variant-root $(VARIANT_ROOT) --tool rtl-lint \
--output $(META_DIR)/rtl-lint-warnings.json
lint: rtl-lint
comp: $(VERILATOR_EMU)
sim: comp
@@ -83,5 +107,6 @@ wave:
clean:
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(BUILD_DIR)
python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(RTL_LINT_DIR)
.PHONY: comp sim clean
.PHONY: lint rtl-lint check-rtl-lint comp sim clean
+11 -10
View File
@@ -16,6 +16,9 @@ sys.path.insert(0, str(ROOT))
from scripts.setup_helpers import atomic_write # noqa: E402
WARNING_TOOLS = ("iverilog", "verilator", "rtl-lint", "vcs", "yosys", "opensta")
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
TIMESTAMP_RE = re.compile(r"^\[[^]]+\]\s*")
LOCATION_RE = re.compile(
@@ -28,6 +31,7 @@ LOCATION_RE = re.compile(
def warning_line(tool: str, line: str) -> tuple[str, str] | None:
patterns = {
"verilator": re.compile(r"%Warning-([A-Z0-9_]+):\s*(.*)"),
"rtl-lint": re.compile(r"%Warning-([A-Z0-9_]+):\s*(.*)"),
"iverilog": re.compile(r"\b(warning|sorry):\s*(.*)", re.IGNORECASE),
"yosys": re.compile(r"\b(?:ABC:\s*)?Warning:\s*(.*)", re.IGNORECASE),
"opensta": re.compile(r"\bWarning\s+(\d+):\s*(.*)", re.IGNORECASE),
@@ -36,7 +40,7 @@ def warning_line(tool: str, line: str) -> tuple[str, str] | None:
match = patterns[tool].search(line)
if not match:
return None
if tool in ("verilator", "opensta"):
if tool in ("verilator", "rtl-lint", "opensta"):
return match.group(1), match.group(2)
if tool == "iverilog":
return match.group(1).lower(), match.group(2)
@@ -84,6 +88,7 @@ def discover(variant_root: Path) -> dict[str, list[Path]]:
return {
"iverilog": sorted((variant_root / "sim/iverilog").glob("*/*.log")),
"verilator": sorted((variant_root / "sim/verilator").glob("*.log")),
"rtl-lint": sorted((variant_root / "lint/verilator").glob("lint.log")),
"vcs": sorted((variant_root / "sim/vcs").glob("*/*.log")),
"yosys": sorted((variant_root / "syn/yosys").glob("*.log")),
"opensta": sorted((variant_root / "sta/opensta").glob("*.log")),
@@ -106,7 +111,7 @@ def write_baseline(args: argparse.Namespace) -> int:
def check(args: argparse.Namespace) -> int:
root = args.root.resolve()
variant_root = args.variant_root.resolve()
tools = [args.tool] if args.tool else ["iverilog", "verilator", "vcs", "yosys", "opensta"]
tools = [args.tool] if args.tool else WARNING_TOOLS
logs = discover(variant_root)
reports: dict[str, object] = {}
failures: list[str] = []
@@ -119,12 +124,8 @@ def check(args: argparse.Namespace) -> int:
baseline_entries: dict[str, int] = {}
if baseline_path.is_file():
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
baseline_entries = {
entry["signature"]: entry["count"] for entry in baseline["entries"]
}
current_entries = {
entry["signature"]: entry["count"] for entry in current["entries"]
}
baseline_entries = {entry["signature"]: entry["count"] for entry in baseline["entries"]}
current_entries = {entry["signature"]: entry["count"] for entry in current["entries"]}
new = sorted(set(current_entries) - set(baseline_entries))
increased = sorted(
signature
@@ -159,14 +160,14 @@ def parse_args() -> argparse.Namespace:
baseline = subparsers.add_parser("baseline")
baseline.add_argument("--root", type=Path, default=ROOT)
baseline.add_argument("--profile", required=True)
baseline.add_argument("--tool", choices=("iverilog", "verilator", "vcs", "yosys", "opensta"), required=True)
baseline.add_argument("--tool", choices=WARNING_TOOLS, required=True)
baseline.add_argument("--log", type=Path, action="append", required=True)
baseline.add_argument("--output", type=Path, required=True)
check_parser = subparsers.add_parser("check")
check_parser.add_argument("--root", type=Path, default=ROOT)
check_parser.add_argument("--profile", required=True)
check_parser.add_argument("--variant-root", type=Path, required=True)
check_parser.add_argument("--tool", choices=("iverilog", "verilator", "vcs", "yosys", "opensta"))
check_parser.add_argument("--tool", choices=WARNING_TOOLS)
check_parser.add_argument("--output", type=Path, required=True)
return parser.parse_args()
+6
View File
@@ -15,7 +15,11 @@ sys.path.insert(0, str(ROOT))
from scripts.check_c_warnings import self_owned_warnings # noqa: E402
RTL_LINT_VALUES = ("SIMU=VERILATOR", "HAVE_SVA=YES", "rtl-lint", "check-rtl-lint")
PR_COMMANDS = (
("configs/ci/hazard3-rv32im-ihp130.mk", RTL_LINT_VALUES),
("configs/ci/hazard3-rv32im-ihp130.mk", ("firmware",)),
("configs/ci/hazard3-rv32im-ihp130-shell.mk", ("firmware",)),
("configs/ci/hazard3-rv32im-ihp130-ip-mdd-shell.mk", ("firmware",)),
@@ -49,6 +53,7 @@ PR_COMMANDS = (
("configs/ci/hazard3-rv32im-ihp130.mk", ("STA=OPENSTA", "sta")),
)
SMOKE_COMMANDS = (
("configs/ci/hazard3-rv32im-ihp130.mk", RTL_LINT_VALUES),
("configs/ci/hazard3-rv32im-ihp130.mk", ("firmware",)),
(
"configs/ci/hazard3-rv32im-ihp130.mk",
@@ -86,6 +91,7 @@ PDK_PR_PROFILES = {
def pdk_pr_commands(profile: str) -> tuple[tuple[str, tuple[str, ...]], ...]:
return (
(profile, RTL_LINT_VALUES),
(profile, ("firmware",)),
(profile, ("SIMU=VERILATOR", "HAVE_SVA=YES", "firmware", "sim")),
(profile, ("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm")),
+79 -6
View File
@@ -31,6 +31,7 @@ from scripts.regress import ( # noqa: E402
NIGHTLY_COMMANDS,
PDK_PR_PROFILES,
PR_COMMANDS,
RTL_LINT_VALUES,
SMOKE_COMMANDS,
pdk_pr_commands,
regression_environment,
@@ -296,9 +297,7 @@ def test_dependency_lock_and_config_key_include_a_fixed_timestamp(tmp_path: Path
lock = load_lock(ROOT / "config/dependencies.lock.json")
assert lock["schema_version"] == 1
assert len(lock["sources"]["mpw"]["revision"]) == 40
assert lock["sources"]["pdk_sky130"]["submodules"] == [
"libraries/sky130_fd_sc_hd/latest"
]
assert lock["sources"]["pdk_sky130"]["submodules"] == ["libraries/sky130_fd_sc_hd/latest"]
command = (
sys.executable,
@@ -360,7 +359,10 @@ def test_regression_runner_uses_one_build_timestamp(monkeypatch) -> None:
def test_verilator_simulations_use_uniform_timeout() -> None:
verilator_makefile = ROOT / "rtl/mini/mk/verilator.mk"
assert "SOC_SIM_TIME ?= 180" in verilator_makefile.read_text(encoding="utf-8")
verilator_source = verilator_makefile.read_text(encoding="utf-8")
assert "SOC_SIM_TIME ?= 180" in verilator_source
assert "RTL_LINT_FLAGS := --lint-only --no-timing" in verilator_source
assert "--assert --Wall" in verilator_source
regression_commands = (*SMOKE_COMMANDS, *PR_COMMANDS, *NIGHTLY_COMMANDS)
for _, values in regression_commands:
@@ -375,12 +377,12 @@ def test_smoke_regression_uses_ihp130_behavioral_coverage_only() -> None:
assert commands == SMOKE_COMMANDS
assert profiles == ()
command_values = [values for _, values in commands]
assert command_values[0] == RTL_LINT_VALUES
assert ("firmware",) in command_values
assert ("SIMU=VERILATOR", "HAVE_SVA=YES", "comp") in command_values
assert ("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm") in command_values
assert not any(
"synth" in values or "sta" in values or "netsim" in values
for values in command_values
"synth" in values or "sta" in values or "netsim" in values for values in command_values
)
dry_run = run(
@@ -421,6 +423,7 @@ def test_pdk_pr_regressions_cover_firmware_rtl_and_netlist() -> None:
for profile in PDK_PR_PROFILES.values():
commands = pdk_pr_commands(profile)
command_values = [values for _, values in commands]
assert command_values[0] == RTL_LINT_VALUES
assert ("firmware",) in command_values
assert any("SIMU=VERILATOR" in values and "firmware" in values for values in command_values)
assert any("SIMU=IVERILOG" in values and "sim-asm" in values for values in command_values)
@@ -429,6 +432,76 @@ def test_pdk_pr_regressions_cover_firmware_rtl_and_netlist() -> None:
assert any("SIMU=IVERILOG" in values and "netsim" in values for values in command_values)
def test_rtl_lint_warning_baseline_is_independent(tmp_path: Path) -> None:
profile = "unit-profile"
lint_log = tmp_path / "variant/lint/verilator/lint.log"
lint_log.parent.mkdir(parents=True)
lint_log.write_text(
f"%Warning-WIDTH: {tmp_path}/rtl/top.sv:12: width mismatch\n",
encoding="utf-8",
)
baseline = tmp_path / f"quality/warnings/{profile}/rtl-lint.json"
run(
sys.executable,
str(ROOT / "scripts/analyze_warnings.py"),
"baseline",
"--root",
str(tmp_path),
"--profile",
profile,
"--tool",
"rtl-lint",
"--log",
str(lint_log),
"--output",
str(baseline),
)
report = tmp_path / "rtl-lint-warnings.json"
run(
sys.executable,
str(ROOT / "scripts/analyze_warnings.py"),
"check",
"--root",
str(tmp_path),
"--profile",
profile,
"--variant-root",
str(tmp_path / "variant"),
"--tool",
"rtl-lint",
"--output",
str(report),
)
assert json.loads(report.read_text(encoding="utf-8"))["status"] == "passed"
lint_log.write_text(
lint_log.read_text(encoding="utf-8")
+ f"%Warning-UNUSED: {tmp_path}/rtl/top.sv:20: unused signal\n",
encoding="utf-8",
)
failed = subprocess.run(
[
sys.executable,
str(ROOT / "scripts/analyze_warnings.py"),
"check",
"--root",
str(tmp_path),
"--profile",
profile,
"--variant-root",
str(tmp_path / "variant"),
"--tool",
"rtl-lint",
"--output",
str(report),
],
text=True,
capture_output=True,
)
assert failed.returncode != 0
assert json.loads(report.read_text(encoding="utf-8"))["failed_tools"] == ["rtl-lint"]
def test_run_flow_writes_structured_result(tmp_path: Path) -> None:
log = tmp_path / "flow.log"
result = tmp_path / "result.json"