Bash for EDA flows
- Write a Bash wrapper that compiles and simulates RTL with Icarus Verilog
- Enable strict mode with
set -euo pipefail - Quote every path
- Parse options with
getopts - Organise steps into functions
- Return exit codes that a regression or CI job can trust
Explanation
Why wrap tool commands
The Linux course showed how to turn a few commands into a script. An EDA flow adds more risk: a compile can fail while the simulator still runs an old binary, a log can report ERROR while the tool exits 0, and a path with a space can silently select the wrong file. A wrapper script makes each step explicit, stops at the first real failure, and leaves a log you can review.
Strict mode
#!/usr/bin/env bash
set -euo pipefail
| Setting | Effect | Why it matters in a flow |
|---|---|---|
-e |
Exit when a command fails, outside tested contexts | A failed compile stops the run |
-u |
Treat an unset variable as an error | A typo such as $TOPP fails instead of expanding to nothing |
-o pipefail |
A pipeline fails if any stage fails | iverilog ... | tee log reports the compiler's failure, not tee's success |
set -e does not trigger inside an if condition or before || and &&. That is useful: you can test a command deliberately. It also means set -e is a safety net, not a replacement for checking results you care about.
Quoting and arrays
Quote every expansion: "$src_dir", "$@". Keep lists of files in arrays so each element stays one argument:
sources=(rtl/counter.v "tb/tb counter.v")
iverilog -g2012 -o build/sim.vvp "${sources[@]}"
"${sources[@]}" expands to exactly two arguments even though the second contains a space. A plain string such as sources="a.v b.v" cannot represent that safely.
Options with getopts
getopts parses short options such as -t top -s seed. A leading : in the option string lets the script report errors itself.
top=tb
seed=1
while getopts ':t:s:h' opt; do
case $opt in
t) top=$OPTARG ;;
s) seed=$OPTARG ;;
h) usage; exit 0 ;;
:) die 2 "option -$OPTARG needs a value" ;;
\?) die 2 "unknown option -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
After shift, the remaining positional arguments ("$@") are the source files.
Functions and exit codes
Use small functions for repeated actions and give each failure class its own status. A documented contract lets a regression runner tell a usage error from a test failure.
| Status | Meaning |
|---|---|
| 0 | Compiled, simulated, and the log contains a PASS marker with no errors |
| 1 | Simulation ran but the test failed |
| 2 | Usage error (bad option or missing file) |
| 3 | Compile failed |
Worked example
Save this as run_sim.sh. It compiles the given sources, runs the simulation with a seed plusarg, writes logs under build/, and decides pass or fail from the log rather than trusting the simulator's exit status alone.
#!/usr/bin/env bash
set -euo pipefail
usage() { printf 'usage: %s [-t top] [-s seed] SOURCE...\n' "${0##*/}"; }
die() { local code=$1; shift; printf 'run_sim: %s\n' "$*" >&2; exit "$code"; }
top=tb
seed=1
while getopts ':t:s:h' opt; do
case $opt in
t) top=$OPTARG ;;
s) seed=$OPTARG ;;
h) usage; exit 0 ;;
:) die 2 "option -$OPTARG needs a value" ;;
\?) die 2 "unknown option -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
(( $# > 0 )) || { usage >&2; exit 2; }
for src in "$@"; do
[[ -f "$src" ]] || die 2 "missing source: $src"
done
build=build
mkdir -p "$build"
vvp_file="$build/$top.vvp"
rm -f "$vvp_file" # never simulate a stale binary
if ! iverilog -g2012 -Wall -s "$top" -o "$vvp_file" "$@" > "$build/compile.log" 2>&1; then
cat "$build/compile.log" >&2
die 3 "compile failed (see $build/compile.log)"
fi
vvp -n "$vvp_file" "+seed=$seed" > "$build/sim.log" 2>&1 || true
if grep -q 'ERROR' "$build/sim.log" || ! grep -q 'TEST PASSED' "$build/sim.log"; then
printf 'FAIL %s seed=%s\n' "$top" "$seed"
exit 1
fi
printf 'PASS %s seed=%s\n' "$top" "$seed"
Run it and check the statuses:
chmod u+x run_sim.sh
./run_sim.sh -t tb_counter rtl/counter.v tb/tb_counter.v; echo "status=$?"
./run_sim.sh -t tb_counter missing.v; echo "status=$?"
./run_sim.sh -x; echo "status=$?"
The testbench must print TEST PASSED only after all checks succeed and print a line containing ERROR for each mismatch. vvp -n makes $stop behave like $finish so a batch run never waits for input. The || true after vvp is deliberate: the verdict comes from the log, which is the only place the testbench reports its checks.
Check yourself
Why does iverilog ... | tee log need pipefail to stop the script on a compile error? Why is the stale .vvp file removed before compiling? Why must the pass/fail decision read the log instead of relying only on the exit status of vvp? What does shift $((OPTIND - 1)) leave in "$@"?
Practice: Bash for EDA flows
Add a -w option that turns compiler warnings into failures: after a successful compile, exit with status 3 if build/compile.log contains the word warning. Then add a -o DIR option for the build directory and confirm that two runs with different directories do not overwrite each other's logs. Test a missing source, a compile error, a failing testbench, and a source path containing a space.
Practice work is non-certified and is not stored by ASIC Campus.