iverilog/vpi/sys_evcd.c

628 lines
20 KiB
C

/*
* Copyright (c) 2026 Stephen Williams (steve@icarus.com)
*
* This source code is free software; you can redistribute it
* and/or modify it in source code form under the terms of the GNU
* General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
# include "sys_priv.h"
/*
* This file implements the IEEE 1364-2005 Clause 18 Extended VCD
* ($dumpports) task family. The port direction comes from the vpiPort
* (vpiPortInfo) meta-data; the value-bearing net is reached through the
* port's vpiLowConn relationship, and its per-bit drive strength is read
* with vpiStrengthVal. The output is byte-compatible (for scalar ports)
* with the GHDL --evcd writer so a single waveform reader works across
* Verilog and VHDL.
*
* PHASE 1: scalar (1-bit) input/output/inout ports -- full pipeline
* (header, $scope/$var/$upscope, initial checkpoint, value changes).
* Vector ports are declared with their range but only bit 0 is dumped;
* the full bus encoding is pinned against the GHDL reference in Phase 2.
*/
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <assert.h>
# include <time.h>
# include "ivl_alloc.h"
static FILE *evcd_file = NULL;
static int evcd_no_date = 0;
static PLI_UINT64 evcd_cur_time = 0;
static int evcd_time_set = 0;
static int evcd_is_off = 0;
static int evcd_started = 0; /* initial checkpoint emitted yet? */
static const char*units_names[] = { "s", "ms", "us", "ns", "ps", "fs" };
/* One tracked port. */
struct evcd_port {
vpiHandle net; /* value net (vpiLowConn of the port) */
vpiHandle inst; /* the dumped instance scope (module side) */
int dir; /* vpiInput / vpiOutput / vpiInout */
unsigned width;
int id; /* EVCD identifier integer */
struct evcd_port*next;
};
static struct evcd_port*evcd_list = NULL;
static struct evcd_port*evcd_tail = NULL; /* append here to keep id order */
static int evcd_next_id = 0;
/* ------------------------------------------------------------------ */
/* State-char and strength mapping (GHDL byte-compatible) */
/* ------------------------------------------------------------------ */
/* Map a 4-state logic value (vpi0/vpi1/vpiZ/vpiX, with vpiH/vpiL folded
onto 1/0) and a port direction to the EVCD state character. */
static char evcd_state_char(int logic, int dir)
{
int v; /* 0, 1, 2=Z, 3=X */
switch (logic) {
case vpi0: case vpiL: v = 0; break;
case vpi1: case vpiH: v = 1; break;
case vpiZ: v = 2; break;
default: v = 3; break;
}
if (dir == vpiInput) {
switch (v) { case 0: return 'D'; case 1: return 'U';
case 2: return 'Z'; default: return 'N'; }
} else if (dir == vpiOutput) {
switch (v) { case 0: return 'L'; case 1: return 'H';
case 2: return 'T'; default: return 'X'; }
} else { /* inout / unknown direction */
switch (v) { case 0: return '0'; case 1: return '1';
case 2: return 'F'; default: return '?'; }
}
}
/* The strength0 / strength1 components for a value, using the
GHDL-compatible convention: a driven level is strong (6), the opposite
rail is high-Z (0); three-state is 0/0; unknown is 6/6. */
static void evcd_strengths(int logic, int*s0, int*s1)
{
switch (logic) {
case vpi0: case vpiL: *s0 = 6; *s1 = 0; break;
case vpi1: case vpiH: *s0 = 0; *s1 = 6; break;
case vpiZ: *s0 = 0; *s1 = 0; break;
default: *s0 = 6; *s1 = 6; break;
}
}
/* Map (input-side value, output-side value) to the IEEE 1364-2005 18.4.3
extended-VCD state character for an inout (unknown-direction) port. Value
codes are 0, 1, 2=X, 3=Z, where Z means that side is not driving. A side
that is hi-Z lets the other side determine the input/output mnemonic; both
driving give the agreement (0/1) or conflict (A/a/B/b/C/c) characters. */
static char evcd_inout_char(int in_v, int out_v)
{
static const char tab[4][4] = {
/* out= 0 1 X Z */
/*in0*/ {'0', 'A', 'a', 'D'},
/*in1*/ {'B', '1', 'b', 'U'},
/*inX*/ {'C', 'c', '?', 'N'},
/*inZ*/ {'L', 'H', 'X', 'F'}
};
return tab[in_v & 3][out_v & 3];
}
/* Reduce a vpi logic value to the 0/1/2=X/3=Z code used above. */
static int evcd_vcode(int logic)
{
switch (logic) {
case vpi0: case vpiL: return 0;
case vpi1: case vpiH: return 1;
case vpiZ: return 3;
default: return 2;
}
}
/* Combine a new driver value into one side's accumulated value. Z means the
driver is not contributing; two real drivers that disagree make X. */
static int evcd_side_combine(int acc, int v)
{
if (v == 3) return acc;
if (acc == 3) return v;
if (acc == v) return acc;
return 2;
}
/* The drive strength level (0..7) encoded in a vpi strength bitmask
(1<<level). Per IEEE 1364-2005 18.4.3.2, levels 5..7 are "strong" and
1..4 are "weak"; this distinction selects the d/u/l/h conflict states. */
static int evcd_strlevel(unsigned mask)
{
int lvl = 0;
while (mask > 1) { mask >>= 1; lvl += 1; }
return lvl;
}
/* Non-zero if scope `s` is the instance `inst` or nested within it. */
static int evcd_scope_within(vpiHandle s, vpiHandle inst)
{
int guard = 0;
while (s && guard < 4096) {
if (vpi_compare_objects(s, inst)) return 1;
s = vpi_handle(vpiScope, s);
guard += 1;
}
return 0;
}
/* For a scalar inout port, separate the external (input-side) and module
(output-side) drives by walking the net's vpiDriver objects and
classifying each by whether its scope lies inside the dumped instance,
then map the pair to the EVCD state character. s0 and s1 receive the
resolved strengths. Returns 0 when the net exposes no drivers (the caller
then falls back to the plain resolved-value encoding). */
static char evcd_inout_state(struct evcd_port*p, int*s0, int*s1)
{
vpiHandle it = vpi_iterate(vpiDriver, p->net);
vpiHandle d;
int in_v = 3, out_v = 3; /* side values: 0,1,2=X,3=Z */
int in_lvl = 0, out_lvl = 0; /* strongest level seen per side */
int any = 0;
char c;
s_vpi_value rv;
if (it == NULL) return 0;
while ((d = vpi_scan(it))) {
s_vpi_value val;
vpiHandle dscope = vpi_handle(vpiScope, d);
int v, lvl;
any = 1;
val.format = vpiStrengthVal;
vpi_get_value(d, &val);
v = evcd_vcode((int)val.value.strength[0].logic);
/* Strength of the driven rail (s1 for a 1, s0 otherwise). */
lvl = evcd_strlevel((v == 1) ? val.value.strength[0].s1
: val.value.strength[0].s0);
if (dscope && evcd_scope_within(dscope, p->inst)) {
out_v = evcd_side_combine(out_v, v);
if (lvl > out_lvl) out_lvl = lvl;
} else {
in_v = evcd_side_combine(in_v, v);
if (lvl > in_lvl) in_lvl = lvl;
}
}
if (!any) return 0;
rv.format = vpiStrengthVal;
vpi_get_value(p->net, &rv);
evcd_strengths((int)rv.value.strength[0].logic, s0, s1);
c = evcd_inout_char(in_v, out_v);
/* When both sides drive the same level (0 or 1) but with different
strength ranges, the IEEE state is d/u (input stronger) or l/h
(output stronger) rather than the plain agreement 0/1. */
if (in_v == out_v && (in_v == 0 || in_v == 1)) {
int in_strong = in_lvl >= 5;
int out_strong = out_lvl >= 5;
if (in_strong && !out_strong)
c = (in_v == 0) ? 'd' : 'u';
else if (!in_strong && out_strong)
c = (in_v == 0) ? 'l' : 'h';
}
return c;
}
/* Emit one value record for a port:
scalar -> p<state> <s0> <s1> <<id>
vector -> p<state...> <s0...> <s1...> <<id>
For an N-bit port the state and each strength component carry N
characters, most-significant bit first (matching the [msb:lsb] $var
declaration), and the encoding is byte-compatible with GHDL --evcd. */
static void evcd_emit_value(struct evcd_port*p)
{
s_vpi_value val;
int logic, s0, s1, idx;
val.format = vpiStrengthVal;
vpi_get_value(p->net, &val);
if (p->width <= 1) {
logic = (int)val.value.strength[0].logic;
evcd_strengths(logic, &s0, &s1);
/* For an inout, separate the two drive sides so genuine
conflicts become the IEEE conflict-state characters
(A/a/B/b/C/c) rather than a generic '?'. Falls back to the
resolved-value encoding when no per-driver info exists. */
if (p->dir == vpiInout) {
int cs0 = s0, cs1 = s1;
char cc = evcd_inout_state(p, &cs0, &cs1);
if (cc) {
fprintf(evcd_file, "p%c %d %d <%d\n",
cc, cs0, cs1, p->id);
return;
}
}
fprintf(evcd_file, "p%c %d %d <%d\n",
evcd_state_char(logic, p->dir), s0, s1, p->id);
return;
}
/* Bit 0 of the strength array is the LSB; emit MSB first. */
fputc('p', evcd_file);
for (idx = (int)p->width - 1 ; idx >= 0 ; idx -= 1)
fputc(evcd_state_char((int)val.value.strength[idx].logic, p->dir),
evcd_file);
fputc(' ', evcd_file);
for (idx = (int)p->width - 1 ; idx >= 0 ; idx -= 1) {
evcd_strengths((int)val.value.strength[idx].logic, &s0, &s1);
fputc('0' + s0, evcd_file);
}
fputc(' ', evcd_file);
for (idx = (int)p->width - 1 ; idx >= 0 ; idx -= 1) {
evcd_strengths((int)val.value.strength[idx].logic, &s0, &s1);
fputc('0' + s1, evcd_file);
}
fprintf(evcd_file, " <%d\n", p->id);
}
/* ------------------------------------------------------------------ */
/* Time + value-change callback */
/* ------------------------------------------------------------------ */
static PLI_UINT64 evcd_now(void)
{
s_vpi_time now;
now.type = vpiSimTime;
vpi_get_time(0, &now);
return ((PLI_UINT64)now.high << 32) | (PLI_UINT64)now.low;
}
static void evcd_emit_time(PLI_UINT64 t)
{
if (!evcd_time_set || t != evcd_cur_time) {
fprintf(evcd_file, "#%" PLI_UINT64_FMT "\n", t);
evcd_cur_time = t;
evcd_time_set = 1;
}
}
static PLI_INT32 evcd_value_cb(struct t_cb_data*cb)
{
struct evcd_port*p = (struct evcd_port*)cb->user_data;
/* Value changes while the design settles at the start time are
folded into the initial checkpoint, so ignore them until it has
been emitted (at the read-only synch of the current step). */
if (evcd_file == NULL || evcd_is_off || !evcd_started)
return 0;
evcd_emit_time(evcd_now());
evcd_emit_value(p);
return 0;
}
/* ------------------------------------------------------------------ */
/* Header */
/* ------------------------------------------------------------------ */
static void evcd_emit_header(void)
{
int prec = vpi_get(vpiTimePrecision, 0);
unsigned scale = 1, udx = 0;
time_t walltime;
time(&walltime);
assert(prec >= -15);
while (prec < 0) { udx += 1; prec += 3; }
while (prec > 0) { scale *= 10; prec -= 1; }
if (!evcd_no_date) {
fprintf(evcd_file, "$date\n\t%s$end\n",
asctime(localtime(&walltime)));
}
fprintf(evcd_file, "$version\n\tIcarus Verilog\n$end\n");
fprintf(evcd_file, "$timescale\n\t%u%s\n$end\n", scale, units_names[udx]);
}
/* ------------------------------------------------------------------ */
/* Scope / port scan */
/* ------------------------------------------------------------------ */
static const char*evcd_dir_name(int dir)
{
switch (dir) {
case vpiInput: return "input";
case vpiOutput: return "output";
case vpiInout: return "inout";
default: return "?";
}
}
/* Declare every port of one module instance scope: emit its $var record,
track it, and register a value-change callback on its net. */
static void evcd_scan_scope(vpiHandle scope)
{
vpiHandle ports, port;
fprintf(evcd_file, "$scope module %s $end\n",
vpi_get_str(vpiFullName, scope));
ports = vpi_iterate(vpiPort, scope);
while (ports && (port = vpi_scan(ports))) {
struct evcd_port*p;
s_cb_data cb;
s_vpi_time tm;
const char*name = vpi_get_str(vpiName, port);
int dir = vpi_get(vpiDirection, port);
unsigned width = vpi_get(vpiSize, port);
vpiHandle net = vpi_handle(vpiLowConn, port);
if (net == NULL) {
vpi_printf("EVCD warning: port %s has no value net; "
"skipping.\n", name ? name : "?");
continue;
}
p = (struct evcd_port*)calloc(1, sizeof *p);
p->net = net;
p->inst = scope;
p->dir = dir;
p->width = width;
p->id = evcd_next_id++;
p->next = NULL;
/* Append so checkpoints emit records in ascending id order
(matching the GHDL --evcd writer). */
if (evcd_tail) evcd_tail->next = p;
else evcd_list = p;
evcd_tail = p;
/* $var port <size> <<id> <reference> $end. Use the port's
actual declared [msb:lsb] range when it is a vector. */
if (width > 1 || vpi_get(vpiLeftRange, net) != 0)
fprintf(evcd_file, "$var port [%d:%d] <%d %s $end\n",
(int)vpi_get(vpiLeftRange, net),
(int)vpi_get(vpiRightRange, net), p->id, name);
else
fprintf(evcd_file, "$var port 1 <%d %s $end\n", p->id, name);
/* Track value changes on the port's value net. */
memset(&cb, 0, sizeof cb);
tm.type = vpiSimTime;
cb.reason = cbValueChange;
cb.cb_rtn = evcd_value_cb;
cb.obj = net;
cb.time = &tm;
cb.value = NULL;
cb.user_data = (PLI_BYTE8*)p;
vpi_register_cb(&cb);
(void)evcd_dir_name; /* reserved for diagnostics */
}
fprintf(evcd_file, "$upscope $end\n");
}
/* Dump the current value of every tracked port (checkpoint). */
static void evcd_checkpoint(void)
{
struct evcd_port*p;
for (p = evcd_list ; p ; p = p->next)
evcd_emit_value(p);
}
/* ------------------------------------------------------------------ */
/* End of simulation */
/* ------------------------------------------------------------------ */
static PLI_INT32 evcd_end_cb(struct t_cb_data*cb)
{
(void)cb;
if (evcd_file) {
fprintf(evcd_file, "#%" PLI_UINT64_FMT "\n", evcd_now());
fflush(evcd_file);
}
return 0;
}
/* The initial checkpoint, deferred to the read-only synch of the start
time so it records settled values: #<t> $dumpports <values> $end. */
static PLI_INT32 evcd_initial_cb(struct t_cb_data*cb)
{
(void)cb;
if (evcd_file == NULL)
return 0;
evcd_cur_time = evcd_now();
evcd_time_set = 1;
fprintf(evcd_file, "#%" PLI_UINT64_FMT "\n$dumpports\n", evcd_cur_time);
evcd_checkpoint();
fprintf(evcd_file, "$end\n");
evcd_started = 1;
return 0;
}
/* ------------------------------------------------------------------ */
/* $dumpports */
/* ------------------------------------------------------------------ */
static PLI_INT32 sys_dumpports_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
vpiHandle systf, argv, arg;
vpiHandle scopes[64];
unsigned nscopes = 0;
char*path = NULL;
s_cb_data cb;
unsigned i;
(void)name;
if (evcd_file != NULL) {
vpi_printf("EVCD warning: $dumpports already active; ignored.\n");
return 0;
}
systf = vpi_handle(vpiSysTfCall, 0);
argv = vpi_iterate(vpiArgument, systf);
/* Arguments are module instances to dump plus, optionally, a
trailing file name (string). Anything that is not a module is
treated as the file-name expression. */
while (argv && (arg = vpi_scan(argv))) {
if (vpi_get(vpiType, arg) == vpiModule) {
if (nscopes < 64)
scopes[nscopes++] = arg;
} else if (path == NULL) {
s_vpi_value v;
v.format = vpiStringVal;
vpi_get_value(arg, &v);
if (v.value.str && v.value.str[0])
path = strdup(v.value.str);
}
}
/* Default scope = the module containing the call; default file. */
if (nscopes == 0) {
vpiHandle sc = vpi_handle(vpiScope, systf);
if (sc) scopes[nscopes++] = sc;
}
if (path == NULL)
path = strdup("dumpports.vcd");
evcd_file = fopen(path, "w");
if (evcd_file == NULL) {
vpi_printf("EVCD error: cannot open %s for output.\n", path);
free(path);
return 0;
}
vpi_printf("EVCD info: dumpports file %s opened for output.\n", path);
free(path);
evcd_emit_header();
for (i = 0 ; i < nscopes ; i += 1)
evcd_scan_scope(scopes[i]);
fprintf(evcd_file, "$enddefinitions $end\n");
/* Defer the initial checkpoint to the read-only synch of the
current time so it captures settled values. */
{
s_vpi_time tm;
tm.type = vpiSimTime;
tm.high = 0;
tm.low = 0;
memset(&cb, 0, sizeof cb);
cb.reason = cbReadOnlySynch;
cb.cb_rtn = evcd_initial_cb;
cb.time = &tm;
cb.obj = NULL;
vpi_register_cb(&cb);
}
/* Close the file cleanly at the end of simulation. */
memset(&cb, 0, sizeof cb);
cb.reason = cbEndOfSimulation;
cb.cb_rtn = evcd_end_cb;
cb.obj = NULL;
vpi_register_cb(&cb);
return 0;
}
/* $dumpportsall: force a checkpoint of all port values now. */
static PLI_INT32 sys_dumpportsall_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
(void)name;
if (evcd_file == NULL) return 0;
evcd_emit_time(evcd_now());
evcd_checkpoint();
return 0;
}
/* $dumpportsoff / $dumpportson: suspend / resume dumping. */
static PLI_INT32 sys_dumpportsoff_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
struct evcd_port*p;
(void)name;
if (evcd_file == NULL || evcd_is_off) return 0;
evcd_emit_time(evcd_now());
/* Checkpoint every bit of every port as X while suspended. */
for (p = evcd_list ; p ; p = p->next) {
int s0, s1;
unsigned b;
char c = evcd_state_char(vpiX, p->dir);
evcd_strengths(vpiX, &s0, &s1);
fputc('p', evcd_file);
for (b = 0 ; b < p->width ; b += 1) fputc(c, evcd_file);
fputc(' ', evcd_file);
for (b = 0 ; b < p->width ; b += 1) fputc('0' + s0, evcd_file);
fputc(' ', evcd_file);
for (b = 0 ; b < p->width ; b += 1) fputc('0' + s1, evcd_file);
fprintf(evcd_file, " <%d\n", p->id);
}
evcd_is_off = 1;
return 0;
}
static PLI_INT32 sys_dumpportson_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
(void)name;
if (evcd_file == NULL || !evcd_is_off) return 0;
evcd_is_off = 0;
evcd_emit_time(evcd_now());
evcd_checkpoint();
return 0;
}
/* $dumpportsflush: flush the output buffer. */
static PLI_INT32 sys_dumpportsflush_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
(void)name;
if (evcd_file) fflush(evcd_file);
return 0;
}
/* $dumpportslimit(size [, file]): accepted for compatibility. The
file-size cap is not enforced in Phase 1. */
static PLI_INT32 sys_dumpportslimit_calltf(ICARUS_VPI_CONST PLI_BYTE8*name)
{
(void)name;
return 0;
}
/* ------------------------------------------------------------------ */
/* Registration */
/* ------------------------------------------------------------------ */
static void register_evcd_task(const char*tfname,
PLI_INT32 (*calltf)(ICARUS_VPI_CONST PLI_BYTE8*))
{
s_vpi_systf_data tf;
memset(&tf, 0, sizeof tf);
tf.type = vpiSysTask;
tf.tfname = (PLI_BYTE8*)tfname;
tf.calltf = calltf;
tf.compiletf = NULL;
tf.sizetf = NULL;
tf.user_data = (PLI_BYTE8*)tfname;
vpi_register_systf(&tf);
}
void sys_evcd_register(void)
{
register_evcd_task("$dumpports", sys_dumpports_calltf);
register_evcd_task("$dumpportsall", sys_dumpportsall_calltf);
register_evcd_task("$dumpportsoff", sys_dumpportsoff_calltf);
register_evcd_task("$dumpportson", sys_dumpportson_calltf);
register_evcd_task("$dumpportsflush", sys_dumpportsflush_calltf);
register_evcd_task("$dumpportslimit", sys_dumpportslimit_calltf);
}