Bash for EDA flows

Learning objectives
  • 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
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

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 "$@"?

Engineering practice

Practice: Bash for EDA flows

Task→Run / reason→Check result→Explain why

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.