Active
Environment Setup
A ground-up CPU RTL design in SystemVerilog — from combinational building blocks through a fully pipelined processor, with open-source tooling.
Install Core Tools
| # Core simulation & synthesis | |
| brew install icarus-verilog verilator yosys make | |
| # Verible (requires dedicated tap on macOS) | |
| brew tap chipsalliance/verible | |
| brew install verible | |
| # Waveform viewer | |
| brew install surfer | |
| # Schematic generation | |
| npm install -g netlistsvg | |
| # Python dependencies for TerosHDL | |
| brew install uv | |
| uv venv ~/.teroshdl-venv | |
| uv pip install --python ~/.teroshdl-venv/bin/python teroshdl vunit-hdl edalize |
TerosHDL Configuration
Install the TerosHDL extension (teros-technology.teroshdl) from VS Code Marketplace, then configure via the TerosHDL sidebar:
| Setting | Value |
|---|---|
| General → Python path | ~/.teroshdl-venv/bin/python |
| General → Make binary directory | /opt/homebrew/opt/make/libexec/gnubin/ |
| General → Simulator | Icarus Verilog |
| Linter → Verilog/SV error linter | Verilator |
| Linter → Verilog/SV style linter | Verible |
| Linter → Verilator → Arguments | -Wall --language 1800-2017 -f yantra-cpu.f |
| Schematic viewer → Backend | Yosys |
| Tools → Icarus Verilog → Installation path | /opt/homebrew/bin |
| Tools → Verilator → Installation path | /opt/homebrew/bin |
| Tools → Verible → Installation path | /opt/homebrew/bin |
| Tools → Yosys → Installation path | /opt/homebrew/bin |
What each tool gives you through TerosHDL:
- Verilator → Real-time error linting (missing signals, width mismatches, latch inference)
- Verible → Style linting (naming conventions, indentation) + auto-formatting + language server (go-to-definition, hover docs)
- Icarus Verilog → Run simulations directly from the sidebar
- Yosys → View synthesized schematic diagrams of your modules
- Surfer → Open
.vcdwaveform files:surfer dump.vcd
Note: The
-f yantra-cpu.fflag tells Verilator to read include paths from the project'syantra-cpu.ffilelist.
VS Code Workspace Settings
.vscode/settings.json:
| { | |
| "files.associations": { | |
| "*.sv": "systemverilog", | |
| "*.svh": "systemverilog" | |
| }, | |
| "[systemverilog]": { | |
| "editor.defaultFormatter": "teros-technology.teroshdl", | |
| "editor.formatOnSave": true | |
| } | |
| } |
Build Script Usage
The build system supports linting (errors + style), simulation (single or batch), synthesis with gate-level stats, SVG schematic generation, waveform viewing, and auto-formatting — all from a single run.sh entry point.
| ./run.sh lint # Verilator (errors) + Verible (style) on all RTL | |
| ./run.sh fmt # Auto-format all .sv files with Verible | |
| ./run.sh sim tb_alu # Compile and simulate a testbench | |
| ./run.sh sim tb_alu --wave # Simulate and open waveform in Surfer | |
| ./run.sh sim:unit # Run all unit testbenches | |
| ./run.sh sim:all # Run all testbenches (unit + integration) | |
| ./run.sh synth alu # Synthesize a module with Yosys | |
| ./run.sh schematic alu # Generate SVG schematic via netlistsvg | |
| ./run.sh check # Verify all tools are installed | |
| ./run.sh clean # Remove build artifacts |
The Running Script
run.sh:
#!/opt/homebrew/bin/bash
# ============================================================================
# Yantra CPU — Build, Lint, Simulate, and View
# Usage: ./run.sh <command> [options]
# ============================================================================
clear
set -euo pipefail
# ── Configuration ───────────────────────────────────────────────────────────
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${PROJECT_ROOT}/build"
RTL_DIR="${PROJECT_ROOT}/rtl"
TB_DIR="${PROJECT_ROOT}/tb"
PROGRAMS_DIR="${TB_DIR}/programs"
# Tools
IVERILOG="iverilog -g2012"
VVP="vvp"
VERILATOR="verilator"
VERIBLE_LINT="verible-verilog-lint"
VERIBLE_FMT="verible-verilog-format"
YOSYS="yosys"
SURFER="surfer"
# Package file (created later — auto-detected if present)
# Packages must be compiled first so other modules can import them
PKG="${RTL_DIR}/common/yantra_pkg.sv"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ── Helper Functions ────────────────────────────────────────────────────────
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
fail() { echo -e "${RED}[FAIL]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
ensure_build_dir() {
mkdir -p "${BUILD_DIR}"
}
# Make a Yosys-compatible copy of an SV file.
# Yosys doesn't support: import statements, custom enum types, or enum values
# from packages. This function strips/replaces them with raw literals.
# The package file itself is read by Yosys unmodified — only module files need this.
yosys_sanitize() {
local src="$1"
local dst="$2"
sed -e 's/^[[:space:]]*import[[:space:]].*;//' \
-e 's/alu_op_t/logic [3:0]/g' \
-e "s/ALU_ADD/4'b0000/g" \
-e "s/ALU_SUB/4'b0001/g" \
-e "s/ALU_AND/4'b0010/g" \
-e "s/ALU_OR/4'b0011/g" \
-e "s/ALU_XOR/4'b0100/g" \
-e "s/ALU_SLL/4'b0101/g" \
-e "s/ALU_SRL/4'b0110/g" \
-e "s/ALU_SRA/4'b0111/g" \
-e "s/ALU_SLT/4'b1000/g" \
-e "s/ALU_NOR/4'b1001/g" \
"$src" > "$dst"
}
# ── Commands ────────────────────────────────────────────────────────────────
cmd_help() {
cat <<EOF
Yantra CPU Build System
Usage: ./run.sh <command> [args]
Commands:
lint Lint all RTL with Verilator (errors) + Verible (style)
lint:errors Lint with Verilator only (catches real bugs)
lint:style Lint with Verible only (style checks)
fmt Auto-format all .sv files with Verible
fmt:check Check formatting without modifying files
sim <testbench> Compile and simulate a testbench (e.g., sim tb_mux)
sim:unit Run all unit testbenches
sim:integration Run all integration testbenches
sim:all Run every testbench (unit + integration)
wave <name> Open waveform in Surfer (e.g., wave tb_mux)
synth <module> Synthesize a module with Yosys and show stats
schematic <module> Generate clean SVG schematic (requires netlistsvg)
check Verify all tools are installed and accessible
clean Remove all build artifacts
help Show this help message
Examples:
./run.sh lint # Lint everything
./run.sh sim tb_mux # Simulate the mux testbench
./run.sh sim tb_mux --wave # Simulate and auto-open waveforms
./run.sh sim:all # Run all tests
./run.sh wave tb_mux # Open existing waveform
./run.sh synth mux # Synthesize mux module
./run.sh schematic mux # Generate mux schematic SVG
./run.sh fmt # Format all source files
EOF
}
cmd_check() {
info "Checking tool installations..."
local all_ok=true
for tool in iverilog vvp verilator verible-verilog-lint verible-verilog-format yosys surfer netlistsvg; do
if command -v "$tool" &>/dev/null; then
pass "$tool → $(command -v "$tool")"
else
fail "$tool → NOT FOUND"
all_ok=false
fi
done
echo ""
if $all_ok; then
pass "All tools installed!"
else
fail "Some tools are missing. Run 'brew install <tool>' to fix."
exit 1
fi
}
# ── Linting ─────────────────────────────────────────────────────────────────
cmd_lint() {
cmd_lint_errors
echo ""
cmd_lint_style
}
cmd_lint_errors() {
info "Linting with Verilator (error checking)..."
local sv_files=()
while IFS= read -r -d '' f; do
sv_files+=("$f")
done < <(find "${RTL_DIR}" -name "*.sv" -print0 | sort -z)
if [[ ${#sv_files[@]} -eq 0 ]]; then
warn "No .sv files found in ${RTL_DIR}"
return
fi
if ${VERILATOR} --lint-only -Wall --language 1800-2017 "${sv_files[@]}" 2>&1; then
pass "Verilator: No errors found"
else
fail "Verilator: Errors detected (see above)"
return 1
fi
}
cmd_lint_style() {
info "Linting with Verible (style checking)..."
local sv_files=()
while IFS= read -r -d '' f; do
sv_files+=("$f")
done < <(find "${RTL_DIR}" -name "*.sv" -print0 | sort -z)
if [[ ${#sv_files[@]} -eq 0 ]]; then
warn "No .sv files found in ${RTL_DIR}"
return
fi
local style_ok=true
for f in "${sv_files[@]}"; do
if ! ${VERIBLE_LINT} "$f" 2>&1; then
style_ok=false
fi
done
if $style_ok; then
pass "Verible: Style checks passed"
else
warn "Verible: Style issues found (see above)"
fi
}
# ── Formatting ──────────────────────────────────────────────────────────────
cmd_fmt() {
info "Formatting all .sv files with Verible..."
local count=0
while IFS= read -r -d '' f; do
${VERIBLE_FMT} --inplace "$f"
((count++))
done < <(find "${RTL_DIR}" "${TB_DIR}" -name "*.sv" -print0 | sort -z)
pass "Formatted ${count} files"
}
cmd_fmt_check() {
info "Checking formatting (no changes)..."
local issues=0
while IFS= read -r -d '' f; do
if ! diff -q <(${VERIBLE_FMT} "$f") "$f" &>/dev/null; then
warn "Needs formatting: $f"
((issues++))
fi
done < <(find "${RTL_DIR}" "${TB_DIR}" -name "*.sv" -print0 | sort -z)
if [[ $issues -eq 0 ]]; then
pass "All files properly formatted"
else
fail "${issues} file(s) need formatting. Run: ./run.sh fmt"
return 1
fi
}
# ── Simulation ──────────────────────────────────────────────────────────────
cmd_sim() {
local tb_name="${1:-}"
local open_wave=false
# Parse flags
for arg in "$@"; do
case "$arg" in
--wave|-w) open_wave=true ;;
esac
done
if [[ -z "$tb_name" ]]; then
fail "Usage: ./run.sh sim <testbench_name> [--wave]"
echo " Example: ./run.sh sim tb_mux"
echo " Example: ./run.sh sim tb_mux --wave"
return 1
fi
ensure_build_dir
# Find the testbench file
local tb_file
tb_file=$(find "${TB_DIR}" -name "${tb_name}.sv" | head -1)
if [[ -z "$tb_file" ]]; then
fail "Testbench not found: ${tb_name}.sv"
echo " Looked in: ${TB_DIR}/"
echo " Available testbenches:"
find "${TB_DIR}" -name "tb_*.sv" -exec basename {} .sv \; | sed 's/^/ /'
return 1
fi
info "Found testbench: ${tb_file}"
# Gather RTL source files (package first, then everything else)
local rtl_files=()
[[ -f "$PKG" ]] && rtl_files+=("$PKG")
while IFS= read -r -d '' f; do
[[ "$f" != "$PKG" ]] && rtl_files+=("$f")
done < <(find "${RTL_DIR}" -name "*.sv" -print0 | sort -z)
local out_file="${BUILD_DIR}/${tb_name}.out"
local vcd_file="${BUILD_DIR}/${tb_name}.vcd"
# Compile
info "Compiling: ${tb_name}..."
if ! ${IVERILOG} -o "${out_file}" "${rtl_files[@]}" "${tb_file}"; then
fail "Compilation failed"
return 1
fi
pass "Compiled → ${out_file}"
# Simulate
info "Simulating: ${tb_name}..."
echo "────────────────────────────────────────"
(cd "${BUILD_DIR}" && ${VVP} "${out_file}")
local sim_exit=$?
echo "────────────────────────────────────────"
if [[ $sim_exit -eq 0 ]]; then
pass "Simulation complete"
else
fail "Simulation exited with code ${sim_exit}"
fi
# Open waveforms if requested and .vcd exists
if $open_wave; then
# Check for .vcd in build dir (testbench may write it there or in cwd)
local found_vcd=""
for candidate in "${vcd_file}" "${BUILD_DIR}/"*.vcd; do
if [[ -f "$candidate" ]]; then
found_vcd="$candidate"
break
fi
done
if [[ -n "$found_vcd" ]]; then
info "Opening waveform: ${found_vcd}"
${SURFER} "${found_vcd}" &>/dev/null &
disown
else
warn "No .vcd file generated. Add \$dumpfile/\$dumpvars to your testbench."
fi
fi
}
cmd_sim_unit() {
info "Running all unit testbenches..."
local pass_count=0
local fail_count=0
while IFS= read -r -d '' tb_file; do
local tb_name
tb_name=$(basename "$tb_file" .sv)
echo ""
info "━━━ ${tb_name} ━━━"
if cmd_sim "$tb_name"; then
((pass_count++))
else
((fail_count++))
fi
done < <(find "${TB_DIR}/unit" -name "tb_*.sv" -print0 2>/dev/null | sort -z)
echo ""
echo "════════════════════════════════════════"
pass "Passed: ${pass_count}"
[[ $fail_count -gt 0 ]] && fail "Failed: ${fail_count}"
echo "════════════════════════════════════════"
[[ $fail_count -eq 0 ]]
}
cmd_sim_integration() {
info "Running all integration testbenches..."
local pass_count=0
local fail_count=0
while IFS= read -r -d '' tb_file; do
local tb_name
tb_name=$(basename "$tb_file" .sv)
echo ""
info "━━━ ${tb_name} ━━━"
if cmd_sim "$tb_name"; then
((pass_count++))
else
((fail_count++))
fi
done < <(find "${TB_DIR}/integration" -name "tb_*.sv" -print0 2>/dev/null | sort -z)
echo ""
echo "════════════════════════════════════════"
pass "Passed: ${pass_count}"
[[ $fail_count -gt 0 ]] && fail "Failed: ${fail_count}"
echo "════════════════════════════════════════"
[[ $fail_count -eq 0 ]]
}
cmd_sim_all() {
cmd_sim_unit
echo ""
cmd_sim_integration
}
# ── Waveform Viewing ────────────────────────────────────────────────────────
cmd_wave() {
local name="${1:-}"
if [[ -z "$name" ]]; then
fail "Usage: ./run.sh wave <testbench_name>"
echo " Available waveforms:"
find "${BUILD_DIR}" -name "*.vcd" -exec basename {} .vcd \; 2>/dev/null | sed 's/^/ /'
return 1
fi
local vcd_file="${BUILD_DIR}/${name}.vcd"
if [[ ! -f "$vcd_file" ]]; then
# Try to find any matching .vcd
vcd_file=$(find "${BUILD_DIR}" -name "${name}*.vcd" | head -1)
fi
if [[ -z "$vcd_file" || ! -f "$vcd_file" ]]; then
fail "No waveform found for: ${name}"
echo " Run './run.sh sim ${name}' first to generate it."
return 1
fi
info "Opening: ${vcd_file}"
${SURFER} "${vcd_file}" &>/dev/null &
disown
}
# ── Synthesis ───────────────────────────────────────────────────────────────
cmd_synth() {
local module_name="${1:-}"
if [[ -z "$module_name" ]]; then
fail "Usage: ./run.sh synth <module_name>"
echo " Example: ./run.sh synth mux"
return 1
fi
ensure_build_dir
# Find the module file
local module_file
module_file=$(find "${RTL_DIR}" -name "${module_name}.sv" | head -1)
if [[ -z "$module_file" ]]; then
fail "Module not found: ${module_name}.sv"
return 1
fi
# Create Yosys-compatible copies (strip import lines)
local yosys_dir="${BUILD_DIR}/yosys_tmp"
mkdir -p "${yosys_dir}"
local read_cmds=""
# Package first (unmodified)
if [[ -f "$PKG" ]]; then
read_cmds+="read_verilog -sv ${PKG}; "
fi
# All other RTL files — sanitize for Yosys compatibility
while IFS= read -r -d '' f; do
if [[ "$f" != "$PKG" ]]; then
local tmp_file="${yosys_dir}/$(basename "$f")"
yosys_sanitize "$f" "$tmp_file"
read_cmds+="read_verilog -sv ${tmp_file}; "
fi
done < <(find "${RTL_DIR}" -name "*.sv" -print0 | sort -z)
local json_out="${BUILD_DIR}/${module_name}_synth.json"
info "Synthesizing ${module_name} with Yosys..."
${YOSYS} -p "${read_cmds} synth -top ${module_name}; stat; write_json ${json_out}" 2>&1 | \
grep -E "^[0-9]|Number|Printing|Warning|Error|stat|==="
# Clean up temp files
rm -rf "${yosys_dir}"
if [[ -f "$json_out" ]]; then
pass "Synthesis complete → ${json_out}"
else
fail "Synthesis failed"
return 1
fi
}
# ── Schematic Generation ────────────────────────────────────────────────────
cmd_schematic() {
local module_name="${1:-}"
if [[ -z "$module_name" ]]; then
fail "Usage: ./run.sh schematic <module_name>"
echo " Example: ./run.sh schematic mux"
return 1
fi
# Check netlistsvg is installed
if ! command -v netlistsvg &>/dev/null; then
fail "netlistsvg not found. Install with: npm install -g netlistsvg"
return 1
fi
ensure_build_dir
# Find the module file
local module_file
module_file=$(find "${RTL_DIR}" -name "${module_name}.sv" | head -1)
if [[ -z "$module_file" ]]; then
fail "Module not found: ${module_name}.sv"
return 1
fi
# Gather all RTL and create Yosys-compatible copies
# Yosys doesn't support 'import' — but it reads packages globally,
# so stripping import lines is safe when the package is read first.
local yosys_dir="${BUILD_DIR}/yosys_tmp"
mkdir -p "${yosys_dir}"
local read_cmds=""
# Package first (unmodified)
if [[ -f "$PKG" ]]; then
read_cmds+="read_verilog -sv ${PKG}; "
fi
# All other RTL files — sanitize for Yosys compatibility
while IFS= read -r -d '' f; do
if [[ "$f" != "$PKG" ]]; then
local tmp_file="${yosys_dir}/$(basename "$f")"
yosys_sanitize "$f" "$tmp_file"
read_cmds+="read_verilog -sv ${tmp_file}; "
fi
done < <(find "${RTL_DIR}" -name "*.sv" -print0 | sort -z)
local json_out="${BUILD_DIR}/${module_name}_netlist.json"
local svg_out="${BUILD_DIR}/${module_name}_schematic.svg"
info "Generating schematic for ${module_name}..."
# Step 1: Yosys — prep (not synth!) preserves high-level cells like $mux
if ! ${YOSYS} -p "${read_cmds} prep -top ${module_name}; write_json -compat-int ${json_out}" 2>&1 | \
grep -E "Warning|Error|Generating" || true; then
true # grep may return non-zero if no matches, that's fine
fi
# Clean up temp files
rm -rf "${yosys_dir}"
if [[ ! -f "${json_out}" ]]; then
fail "Yosys JSON generation failed"
return 1
fi
# Step 2: netlistsvg — renders proper digital logic symbols
if netlistsvg "${json_out}" -o "${svg_out}" 2>&1; then
pass "Schematic generated → ${svg_out}"
info "Opening schematic..."
open "${svg_out}" &>/dev/null &
disown
else
fail "netlistsvg rendering failed"
return 1
fi
}
# ── Clean ───────────────────────────────────────────────────────────────────
cmd_clean() {
info "Cleaning build artifacts..."
rm -rf "${BUILD_DIR}"
pass "Build directory removed"
}
# ── Main Dispatcher ─────────────────────────────────────────────────────────
main() {
local cmd="${1:-help}"
shift 2>/dev/null || true
case "$cmd" in
help|--help|-h) cmd_help ;;
check) cmd_check ;;
lint) cmd_lint ;;
lint:errors) cmd_lint_errors ;;
lint:style) cmd_lint_style ;;
fmt) cmd_fmt ;;
fmt:check) cmd_fmt_check ;;
sim) cmd_sim "$@" ;;
sim:unit) cmd_sim_unit ;;
sim:integration) cmd_sim_integration ;;
sim:all) cmd_sim_all ;;
wave) cmd_wave "$@" ;;
synth) cmd_synth "$@" ;;
schematic) cmd_schematic "$@" ;;
clean) cmd_clean ;;
*)
fail "Unknown command: ${cmd}"
echo "Run './run.sh help' for available commands."
exit 1
;;
esac
}
main "$@"Was this helpful?