docs: rebuild system report and test workflow

This commit is contained in:
2026-07-15 03:48:19 +08:00
parent 1b51f28263
commit 2359e7976c
21 changed files with 1176 additions and 516 deletions

View File

@@ -0,0 +1,45 @@
# Usage from repository root:
# vivado -mode batch -source doc/reports/system/tools/collect_report_evidence.tcl
set script_dir [file dirname [file normalize [info script]]]
set repo_root [file normalize [file join $script_dir ../../../..]]
set xpr [file join $repo_root fpga project Loongson_Soc.xpr]
set out_dir [file join $repo_root doc reports system evidence vivado]
file mkdir $out_dir
if {![file exists $xpr]} {
puts stderr "ERROR: project not found: $xpr"
puts stderr "Run: cd fpga && vivado -mode batch -source create_project.tcl"
exit 2
}
open_project $xpr
set run [get_runs impl_1]
if {[get_property PROGRESS $run] ne "100%"} {
puts stderr "ERROR: impl_1 is not complete; run fpga/run_impl.tcl first"
close_project
exit 3
}
open_run impl_1
report_timing_summary -delay_type min_max -report_unconstrained -check_timing_verbose \
-max_paths 20 -input_pins -file [file join $out_dir timing_summary_full.rpt]
report_utilization -hierarchical -hierarchical_depth 4 \
-file [file join $out_dir utilization_hierarchical.rpt]
report_utilization -file [file join $out_dir utilization.rpt]
report_power -file [file join $out_dir power.rpt]
report_clock_interaction -delay_type min_max -file [file join $out_dir clock_interaction.rpt]
report_cdc -details -file [file join $out_dir cdc.rpt]
report_drc -file [file join $out_dir drc.rpt]
report_methodology -file [file join $out_dir methodology.rpt]
report_route_status -file [file join $out_dir route_status.rpt]
set fp [open [file join $out_dir revision.txt] w]
puts $fp "Evidence generated from implemented design."
puts $fp "Vivado version: [version -short]"
puts $fp "Project: $xpr"
close $fp
close_project
puts "Report evidence written to: $out_dir"
exit 0

View File

@@ -0,0 +1,46 @@
# Force a clean implementation and generate the bitstream used by board tests.
set script_dir [file dirname [file normalize [info script]]]
set repo_root [file normalize [file join $script_dir ../../../..]]
set xpr [file join $repo_root fpga project Loongson_Soc.xpr]
set dsp_report [file join $repo_root fpga project dsp_utilization.rpt]
if {![file exists $xpr]} {
puts stderr "ERROR: project not found: $xpr"
exit 2
}
open_project $xpr
reset_run synth_1
launch_runs synth_1
wait_on_run synth_1
set synth_run [get_runs synth_1]
set synth_progress [get_property PROGRESS $synth_run]
set synth_status [get_property STATUS $synth_run]
puts "synth_1 progress: $synth_progress"
puts "synth_1 status: $synth_status"
if {$synth_progress ne "100%" || [string first "ERROR" [string toupper $synth_status]] >= 0} {
puts stderr "ERROR: fresh synthesis failed"
close_project
exit 3
}
launch_runs impl_1 -to_step write_bitstream
wait_on_run impl_1
set run [get_runs impl_1]
set progress [get_property PROGRESS $run]
set status [get_property STATUS $run]
puts "impl_1 progress: $progress"
puts "impl_1 status: $status"
if {$progress ne "100%" || [string first "ERROR" [string toupper $status]] >= 0} {
puts stderr "ERROR: fresh implementation failed"
close_project
exit 4
}
open_run impl_1
report_utilization -file $dsp_report
close_project
exit 0

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail
# Vivado 2019.2 invokes the host gcc while elaborating XSIM-generated C.
# A read-only ccache directory otherwise surfaces only as XSIM 43-3409.
export CCACHE_DISABLE=1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
REPORT_DIR="$ROOT/doc/reports/system"
EVIDENCE_DIR="$REPORT_DIR/evidence"
SOFTWARE_EXAMPLES=(asm fft fft_dma mlkem_test mlkem_hello)
find_vivado() {
if [[ -n "${VIVADO:-}" && -x "${VIVADO}" ]]; then
printf '%s\n' "$VIVADO"
return 0
fi
if command -v vivado >/dev/null 2>&1; then
command -v vivado
return 0
fi
local candidate
for candidate in \
/opt/Xilinx/Vivado/2019.2/bin/vivado \
/tools/Xilinx/Vivado/2019.2/bin/vivado; do
if [[ -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
require_vivado() {
if ! VIVADO_BIN="$(find_vivado)"; then
echo "ERROR: Vivado not found." >&2
echo "Set VIVADO=/path/to/Vivado/bin/vivado or source Vivado settings64.sh." >&2
exit 127
fi
}
usage() {
cat <<'EOF'
Usage: run_report_tests.sh <command>
Commands:
prepare Create evidence directories and Vivado project if absent
lint Run the project-aware Verilator lint check
build Build asm, fft, fft_dma, mlkem_test and mlkem_hello
soc Build all five; simulate all except board-only asm
implement Run Vivado implementation and collect PPA/CDC/DRC reports
refresh Parse collected evidence, regenerate figures and build the PDF
quick Run lint and refresh only; does not run Vivado implementation
Board-only tests (matmul official 5000-group timing and reset behavior) are
listed in REPORT_RERUN_CHECKLIST.md and are intentionally not automated here.
EOF
}
prepare() {
mkdir -p "$EVIDENCE_DIR/build" "$EVIDENCE_DIR/soc" \
"$EVIDENCE_DIR/board" "$EVIDENCE_DIR/vivado"
if [[ ! -f "$ROOT/fpga/project/Loongson_Soc.xpr" ]]; then
require_vivado
(cd "$ROOT/fpga" && "$VIVADO_BIN" -mode batch -source create_project.tcl)
fi
}
lint() {
prepare
python3 "$ROOT/fpga/run-linter.py" "$ROOT/fpga/project/Loongson_Soc.xpr" \
2>&1 | tee "$EVIDENCE_DIR/lint.log"
}
build_example() {
local example="$1"
local example_dir="$ROOT/sdk/software/examples/$example"
local build_log="$EVIDENCE_DIR/build/$example.log"
make -C "$example_dir" LA32RSOC_WINDOWS_HOME="$ROOT" clean \
2>&1 | tee "$build_log"
make -C "$example_dir" LA32RSOC_WINDOWS_HOME="$ROOT" all \
2>&1 | tee -a "$build_log"
}
build() {
prepare
local example
for example in "${SOFTWARE_EXAMPLES[@]}"; do
build_example "$example"
done
}
run_soc_case() {
local example="$1"
require_vivado
build_example "$example"
(cd "$ROOT/fpga" && "$VIVADO_BIN" -mode batch \
-source "$SCRIPT_DIR/run_soc_sim.tcl") 2>&1 | tee "$EVIDENCE_DIR/soc/$example.log"
}
soc() {
prepare
build_example asm
run_soc_case fft
run_soc_case fft_dma
run_soc_case mlkem_test
run_soc_case mlkem_hello
}
implement() {
prepare
require_vivado
"$VIVADO_BIN" -mode batch -source "$SCRIPT_DIR/run_fresh_impl.tcl" \
2>&1 | tee "$EVIDENCE_DIR/vivado/implementation.log"
"$VIVADO_BIN" -mode batch -source "$SCRIPT_DIR/collect_report_evidence.tcl" \
2>&1 | tee "$EVIDENCE_DIR/vivado/collection.log"
python3 "$ROOT/fpga/check_dsp.py" "$ROOT/fpga/project/dsp_utilization.rpt" 0 \
2>&1 | tee "$EVIDENCE_DIR/vivado/dsp_check.log"
}
refresh() {
make -C "$REPORT_DIR" metrics all
}
case "${1:-}" in
prepare) prepare ;;
lint) lint ;;
build) build ;;
soc) soc ;;
implement) implement ;;
refresh) refresh ;;
quick) lint; refresh ;;
*) usage; exit 2 ;;
esac

View File

@@ -0,0 +1,41 @@
# Runs the currently built sdk/axi_ram.mif with the generic SoC testbench.
# The caller is responsible for building the desired software example first.
set script_dir [file dirname [file normalize [info script]]]
set repo_root [file normalize [file join $script_dir ../../../..]]
set xpr [file join $repo_root fpga project Loongson_Soc.xpr]
# xelab inherits this when compiling generated C files with the host gcc.
set ::env(CCACHE_DISABLE) 1
# Vivado 2019.2's bundled clang needs the matching 64-bit ncurses/tinfo pair.
# On hosts that also carry /lib/libtinfo.so.5, the loader may otherwise mix it
# with /lib64/libncurses.so.5 and fail with an unresolved _nc_tiparm symbol.
if {[info exists ::env(LD_LIBRARY_PATH)] && $::env(LD_LIBRARY_PATH) ne ""} {
append ::env(LD_LIBRARY_PATH) ":/lib64"
} else {
set ::env(LD_LIBRARY_PATH) "/lib64"
}
if {![file exists $xpr]} {
puts stderr "ERROR: project not found: $xpr"
exit 2
}
if {![file exists [file join $repo_root sdk axi_ram.mif]]} {
puts stderr "ERROR: sdk/axi_ram.mif not found; build a software example first"
exit 3
}
open_project $xpr
set simset [get_filesets sim_1]
set_property top tb_top $simset
set_property incremental false $simset
set_property xsim.elaborate.mt_level off $simset
set_property xsim.elaborate.debug_level off $simset
set_property xsim.simulate.log_all_signals false $simset
update_compile_order -fileset sim_1
launch_simulation
run all
close_sim
close_project
exit 0

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Refresh report_metrics.tex from versioned Vivado and test evidence."""
from __future__ import annotations
import datetime as dt
import re
import subprocess
from pathlib import Path
REPORT_DIR = Path(__file__).resolve().parent.parent
ROOT = REPORT_DIR.parents[2]
METRICS = REPORT_DIR / "report_metrics.tex"
def read_first(*paths: Path) -> str:
for path in paths:
if path.is_file():
return path.read_text(encoding="utf-8", errors="replace")
return ""
def set_macro(text: str, name: str, value: str) -> str:
pattern = re.compile(r"(\\newcommand\{\\" + re.escape(name) + r"\}\{)[^\n}]*(\})")
if not pattern.search(text):
raise ValueError(f"macro not found: {name}")
return pattern.sub(lambda m: m.group(1) + value + m.group(2), text, count=1)
def number(value: str) -> str:
raw = value.replace(",", "").strip()
if re.fullmatch(r"-?\d+", raw):
return f"{int(raw):,}"
return raw
def capture(pattern: str, text: str, group: int = 1) -> str | None:
match = re.search(pattern, text, re.MULTILINE)
return match.group(group).strip() if match else None
def update_timing(metrics: str, timing: str) -> str:
summary = capture(r"^\s*(-?\d+\.\d+)\s+-?\d+\.\d+\s+\d+\s+\d+\s+", timing)
if summary:
metrics = set_macro(metrics, "OverallWNS", summary)
for clock, prefix in (("cpu_clk", "Cpu"), ("sys_clk", "Sys"), ("mlkem_clk", "Mlkem")):
freq = capture(r"^\s*" + clock + r"\s+\{[^\n]+?\}\s+\d+\.\d+\s+(\d+\.\d+)", timing)
wns = capture(r"^\s*" + clock + r"\s+(-?\d+\.\d+)\s+", timing)
if freq:
metrics = set_macro(metrics, prefix + "FreqMHz", freq)
if wns:
metrics = set_macro(metrics, prefix + "WNS", wns)
return metrics
def update_util(metrics: str, util: str) -> str:
rows = {
"SliceLUTs": r"^\| Slice LUTs\s+\|\s*([\d,]+)\s*\|",
"SliceFFs": r"^\| Slice Registers\s+\|\s*([\d,]+)\s*\|",
"BramTiles": r"^\| Block RAM Tile\s+\|\s*([\d.]+)\s*\|",
"DspCount": r"^\| DSPs?\s+\|\s*([\d,]+)\s*\|",
}
pct = {
"SliceLUTPct": r"^\| Slice LUTs\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$",
"SliceFFPct": r"^\| Slice Registers\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$",
"BramPct": r"^\| Block RAM Tile\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$",
}
for name, pattern in rows.items():
value = capture(pattern, util)
if value:
metrics = set_macro(metrics, name, number(value))
for name, pattern in pct.items():
value = capture(pattern, util)
if value:
metrics = set_macro(metrics, name, value + r"\%")
return metrics
def update_power(metrics: str, power: str) -> str:
mappings = {
"TotalPower": r"\| Total On-Chip Power \(W\)\s*\|\s*([\d.]+)",
"DynamicPower": r"\| Dynamic \(W\)\s*\|\s*([\d.]+)",
"StaticPower": r"\| Device Static \(W\)\s*\|\s*([\d.]+)",
}
for name, pattern in mappings.items():
value = capture(pattern, power)
if value:
metrics = set_macro(metrics, name, value)
confidence = capture(r"\| Confidence Level\s*\|\s*([^|]+)\|", power)
if confidence:
metrics = set_macro(metrics, "PowerConfidence", confidence + "(未回标活动率)")
return metrics
def update_mlkem(metrics: str, log: str) -> str:
names = {
("512", "SW"): "MlkemFiveTwelveSW",
("512", "CPU"): "MlkemFiveTwelveCPU",
("512", "DMA"): "MlkemFiveTwelveDMA",
("768", "SW"): "MlkemSevenSixtyEightSW",
("768", "CPU"): "MlkemSevenSixtyEightCPU",
("768", "DMA"): "MlkemSevenSixtyEightDMA",
("1024", "SW"): "MlkemTenTwentyFourSW",
("1024", "CPU"): "MlkemTenTwentyFourCPU",
("1024", "DMA"): "MlkemTenTwentyFourDMA",
}
for (level, path), name in names.items():
value = capture(rf"^MLKEM{level}_{path}_ONLY_AVG_CYCLES=(\d+)$", log)
if value:
metrics = set_macro(metrics, name, number(value))
return metrics
def update_fft(metrics: str, cpu_log: str, dma_log: str) -> str:
cpu_ns = capture(r"Hardware FFT Time:\s*(\d+) ns", cpu_log)
dma_ns = capture(r"Hardware FFT Time:\s*(\d+) ns", dma_log)
sw_ns = capture(r"Software FFT Time:\s*(\d+) ns", dma_log or cpu_log)
cpu_speed = capture(r"Speedup Ratio\s*:\s*([\d.]+)x", cpu_log)
dma_speed = capture(r"Speedup Ratio\s*:\s*([\d.]+)x", dma_log)
for name, value in (("FftCpuNs", cpu_ns), ("FftDmaNs", dma_ns), ("FftSoftwareNs", sw_ns)):
if value:
metrics = set_macro(metrics, name, number(value))
for name, value in (("FftCpuSpeedup", cpu_speed), ("FftDmaSpeedup", dma_speed)):
if value:
metrics = set_macro(metrics, name, value)
err_match = re.search(r"Max Abs Error\s*:\s*Re=(\d+),\s*Im=(\d+)", dma_log)
if err_match:
metrics = set_macro(metrics, "FftMaxError", f"Re={err_match.group(1)},Im={err_match.group(2)}")
return metrics
def update_matmul(metrics: str, log: str) -> str:
elapsed = capture(r"Elapsed time:\s*([\d.]+) ms", log)
score = capture(r"Score percent:\s*([\d.]+)", log)
if elapsed:
metrics = set_macro(metrics, "MatmulBoardMs", elapsed)
if score:
metrics = set_macro(metrics, "MatmulScore", score + r"\%")
if "CRC32 matched." in log:
metrics = set_macro(metrics, "MatmulCrcStatus", "PASS")
return metrics
def main() -> None:
metrics = METRICS.read_text(encoding="utf-8")
vivado = REPORT_DIR / "evidence" / "vivado"
legacy = ROOT / "fpga" / "vivado_reports_nodsp"
metrics = update_timing(metrics, read_first(vivado / "timing_summary_full.rpt", legacy / "timing_summary_full.rpt"))
metrics = update_util(metrics, read_first(vivado / "utilization.rpt", legacy / "utilization.rpt"))
metrics = update_power(metrics, read_first(vivado / "power.rpt", legacy / "power.rpt"))
soc = REPORT_DIR / "evidence" / "soc"
metrics = update_mlkem(metrics, read_first(soc / "mlkem_test.log", ROOT / "sdk/software/examples/mlkem_test/result.txt"))
metrics = update_fft(
metrics,
read_first(soc / "fft.log", ROOT / "sdk/software/examples/fft/result.txt"),
read_first(soc / "fft_dma.log", ROOT / "sdk/software/examples/fft_dma/result.txt"),
)
metrics = update_matmul(metrics, read_first(REPORT_DIR / "evidence" / "board" / "matmul_official.log"))
try:
revision = subprocess.check_output(
["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"], text=True
).strip()
except (OSError, subprocess.CalledProcessError):
revision = "unknown"
source = "当前 evidence" if (vivado / "timing_summary_full.rpt").is_file() else "历史报告,待复测"
metrics = set_macro(metrics, "EvidenceRevision", f"{revision}({source})")
metrics = set_macro(metrics, "EvidenceDate", dt.date.today().isoformat())
METRICS.write_text(metrics, encoding="utf-8")
print(f"updated {METRICS}")
if __name__ == "__main__":
main()