Perl in EDA flows
- Read and maintain the Perl scripts that run older chip-design flows: recognise common legacy idioms
- Parse options with
Getopt::Long - Run tools with
systemusing a list of arguments and decode$?into an exit code - Capture tool output safely
- Generate files from here-documents
- Build a seeded Icarus Verilog regression runner in Perl
- Decide when a Perl script should be ported to Python
Explanation
Reading legacy Perl
Inherited scripts use idioms that look cryptic until you know them:
| Idiom | Meaning |
|---|---|
while (<>) { ... } |
Read every line of the files named in @ARGV, or standard input, into $_ |
chomp; next if /^\s*#/; |
Strip the newline from $_; skip comment lines |
my @list = qw(tb_counter tb_fifo); |
A list of words without quotes and commas |
print "x\n" unless $quiet; |
unless is if not |
local $/; my $text = <$fh>; |
Read a whole file at once (the line separator is temporarily undefined) |
$ENV{PROJ_ROOT}, $0, @ARGV |
Environment, script name, arguments |
my @files = glob("rtl/*.v"); |
File-name wildcard expansion |
__DATA__ at the end of a file |
Lines after it are readable through the DATA handle |
$x =~ tr/a-z/A-Z/; |
Character-by-character translation |
When you take over a script, first add use strict; use warnings;, run perl -c script.pl to check syntax, and fix every reported problem before changing behaviour.
Options with Getopt::Long
use Getopt::Long;
my $seeds = 3;
my $out = 'build';
my @tests;
GetOptions(
'seeds=i' => \$seeds, # =i: an integer is required
'out=s' => \$out, # =s: a string is required
'test=s' => \@tests, # may be given several times
'verbose' => \my $verbose,
) or usage();
GetOptions removes the options it recognises from @ARGV and returns false for an unknown option or a bad value, so the script can print usage and stop. Options can be written --seeds 4 or --seeds=4.
Running tools and reading $?
system('iverilog', '-g2012', '-o', $vvp, @sources); # list form: no shell
my $status = $?;
With more than one argument, system runs the program directly. File names containing spaces or characters such as ; are passed exactly and never re-read by a shell. With a single string, system("iverilog $files"), a shell parses the line, which breaks on unusual names and can run unintended commands.
system returns, and stores in $?, a raw wait status rather than the exit code:
| Test | Meaning |
|---|---|
$? == -1 |
The program could not be started; $! says why |
$? & 127 |
Non-zero: killed by that signal number |
$? >> 8 |
The program's exit code |
Capturing output
Backticks, my @lines = `vvp sim.vvp`; or the equivalent qx(vvp sim.vvp), run a command through the shell and return its standard output only; standard error still goes to the terminal. To keep both streams in a log without building a shell string, pass the arguments to sh as "$@":
system('sh', '-c', 'exec "$@" > "$0" 2>&1', $log, @cmd);
Inside sh -c, the first argument after the script becomes $0 (here the log name) and the rest become "$@", so each argument stays exactly as given.
Generating files with here-documents
open(my $fh, '>', "build/$test.f") or die "cannot write build/$test.f: $!\n";
print {$fh} <<"END";
// generated by $0 - do not edit
+incdir+rtl
@{[ join "\n", @sources ]}
tb/$test.v
END
close $fh;
<<"END" interpolates variables like a double-quoted string; <<'END' keeps the text literally, which is what you want for generated Tcl or shell code containing $. @{[ ... ]} inserts the result of an expression.
When to port
Keep a Perl script when it works, is tested and rarely changes. Port it to Python when it needs significant new features, parallel runs, structured JSON or CSV output, or when nobody on the team is comfortable maintaining it. Port by behaviour: record the old script's outputs and exit codes for a set of inputs, then check that the new one matches.
Worked example
The project has rtl/*.v and one testbench per test in tb/, such as tb/tb_fifo.v. A passing testbench prints TEST PASSED; it reads its seed with $value$plusargs("seed=%d", seed). run_regress.pl compiles each test once, runs it with several seeds, keeps a log per run, and exits 0 when everything passed, 1 when anything failed, and 2 for a usage error.
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use File::Path qw(make_path);
sub usage {
print STDERR "usage: run_regress.pl [--seeds N] [--out DIR] TEST...\n";
exit 2;
}
# Convert the raw wait status in $? into a shell-style exit code.
sub exit_code {
my ($status) = @_;
return 127 if $status == -1;
return 128 + ($status & 127) if $status & 127;
return $status >> 8;
}
# Run a command without a shell parsing its arguments; stdout and stderr go to $log.
sub run_logged {
my ($log, @cmd) = @_;
system('sh', '-c', 'exec "$@" > "$0" 2>&1', $log, @cmd);
return exit_code($?);
}
sub log_passed {
my ($log) = @_;
open(my $fh, '<', $log) or return 0;
my ($passed, $error) = (0, 0);
while (my $line = <$fh>) {
$passed = 1 if $line =~ /\bTEST PASSED\b/;
$error = 1 if $line =~ /\b(?:ERROR|FATAL|TEST FAILED)\b/;
}
close $fh;
return $passed && !$error;
}
my $seeds = 3;
my $out = 'build/regress';
GetOptions('seeds=i' => \$seeds, 'out=s' => \$out) or usage();
usage() unless @ARGV && $seeds > 0;
my @rtl = glob('rtl/*.v');
my ($runs, @failures) = (0);
for my $test (@ARGV) {
my $dir = "$out/$test";
make_path($dir);
my $vvp = "$dir/sim.vvp";
my $rc = run_logged("$dir/compile.log",
'iverilog', '-g2012', '-s', $test, '-o', $vvp, @rtl, "tb/$test.v");
if ($rc != 0) {
push @failures, "COMPILE $test (rc=$rc, see $dir/compile.log)";
next;
}
for my $seed (1 .. $seeds) {
my $log = "$dir/seed$seed.log";
$rc = run_logged($log, 'vvp', '-n', $vvp, "+seed=$seed");
$runs++;
next if $rc == 0 && log_passed($log);
push @failures, "FAIL $test seed=$seed (rc=$rc, see $log)";
}
}
printf "%d runs, %d failures\n", $runs, scalar @failures;
print "$_\n" for @failures;
exit(@failures ? 1 : 0);
Run it:
perl run_regress.pl --seeds 2 tb_counter tb_fifo; echo "status=$?"
perl run_regress.pl --seeds two tb_counter; echo "status=$?"
A compile error is recorded once and the test's seeds are skipped. A run passes only when vvp exits 0 and its log contains TEST PASSED without ERROR, FATAL or TEST FAILED, so a simulation that stopped early cannot pass. --seeds two fails the =i check, so GetOptions returns false and the script exits 2. Each failure line names the log to open.
Check yourself
Why is system('iverilog', @args) safer than system("iverilog @args")? What three cases must you check when decoding $?, and how do you get the exit code? What do backticks capture, and what do they miss? Why does the log name in sh -c 'exec "$@" > "$0" 2>&1' come before the command? What is the difference between <<"END" and <<'END'? What does GetOptions return for an unknown option?
Practice: Perl in EDA flows
Add a repeatable --define NAME=VALUE option that passes -DNAME=VALUE to iverilog for every test. Write the summary to $out/summary.txt as well as the terminal. Then take a legacy script from your project or this lesson, record its output and exit code for a passing run, a failing run and a missing testbench, and port it to Python in the style of the regression-runner lesson so that all three cases produce the same exit codes.
Practice work is non-certified and is not stored by ASIC Campus.