JanusHDL

The waveform viewer

Last updated: 

The Waves tab shows the signals you choose, one row each, with the value at the cursor and the wave over time. It shows simulations run in the window as they run, waveform databases written by jhdl --wave, and VCD files.

Opening a waveform

  • Run a simulation in the window (Running simulations): its waveform opens as it runs.
  • File → Open waveform… (Ctrl+O, or the first toolbar button) opens a waveform database (.jhw) or a VCD file (.vcd).
  • Drop a .jhw or .vcd file on the window, or start the window with it: jhdlw run.jhw.

Files are read in the background, with a progress bar in the status bar; Escape cancels a load. A waveform database that jhdl --wave is still writing opens too: the status bar says recording, and the view follows the file as the simulation adds to it.

File → Reload waveform (Ctrl+R) reads the file again, after you ran the simulation again, for example. The signals that still exist stay in the view, by their path, and the zoom and cursor are kept. For a simulation run in the window, Ctrl+R runs it again.

The wave view

Each row has three parts: the signal's Name, its Value at the cursor, and its wave. Drag the lines between the columns to resize them. Hover over a name to see the signal's full path.

The time axis runs along the top. The area after the last recorded time is shaded. Until you add signals, the view says how: Double-click signals in the Signals panel to add them here.

Adding signals

  1. In the Hierarchy tab, click a scope: the top level, an instance, a generate block, or a package under Packages. When a waveform opens, the Signals tab starts at the first scope that has signals.
  2. The Signals tab lists the scope's signals, records and arrays, in the order they are declared, each with its type: std_logic, unsigned(7 downto 0), sfixed(3 downto -4), state_t, record bus_t. Signals already in the wave view are highlighted.
  3. Double-click a signal to add it to the wave view. The button beside the filter box adds every signal listed; so does Signals → Add all signals of the selected scope.

To find a signal in a large scope, type in the filter box (Ctrl+F puts the keyboard there): the list shows only the signals whose names contain what you type.

Removing, reordering and picking rows

  • Select a row by clicking its name or its wave; ↑ and ↓ select the row above or below.
  • Pick several rows: Ctrl+click names to pick them one by one, Shift+click to pick a run of rows from the selected one.
  • Remove the selected or picked rows with Delete, or right-click a name and choose Remove. Signals → Remove all signals empties the view.
  • Reorder rows by dragging a name up or down; a line shows where it will go. An opened row moves with its parts.

Values of every VHDL type

Signals of a simulation show as their VHDL types read:

Type How it shows
std_logic, std_ulogic, bit, boolean A logic wave (see the table below)
Vectors (std_logic_vector, unsigned, signed, bit_vector, sfixed, ufixed, float, ...) A segment per value, in the row's radix. The name keeps the vector's own index range: count[7:0], fx[3:-4], up[0:3]
Enumerations Their literals: idle, busy
Integers Decimal numbers
Reals Numbers
time and other physical types With their primary unit: 25 ns
Strings As text
Records and arrays One row with the whole value as an aggregate, (1, A5, 7, busy), that opens into a row per field or element

How values are drawn

Signal Drawing
One bit, 1 / 0 A high or low line, with a light fill under high
One bit, H / L (weak) As 1 / 0, in a lighter color
One bit, Z A line at mid height, yellow
One bit, U, X, W or - A red block
Vector, real, string, enumeration A segment per value with the value inside; a value too long for its segment ends with …
Vector with any U, X, W or - bit A red segment
Vector with every bit Z A yellow line at mid height
An event (from a Verilog VCD file) An arrow at each time it was triggered
Many changes within one pixel A solid block: zoom in to see them

In hexadecimal and octal, each group of bits shows as one digit. A group with unknown bits shows their common value (Z when all of them are Z), or X if they differ. Decimal shows X, or the common value, if any bit is not 0 or 1.

Radixes

Right-click the name of a vector to choose its radix:

Radix Shows 11101010 as
Binary 11101010
Octal 352
Hexadecimal (the default) EA
Unsigned decimal 234
Signed decimal (two's complement) -22
ASCII (a character per 8 bits) "." (x"4869" shows as "Hi"; characters that cannot be printed show as .)
Gray code (the number it encodes) 179
BCD (a decimal digit per 4 bits) ?? (x"42" shows as 42)
Fixed point -1.375 (signed, 4 fraction bits)
Floating point A 32-bit vector holding 3.25 in single precision shows 3.25
  • Fixed point opens a menu: Signed or Unsigned, and the number of Fraction bits (negative: the value is scaled up).
  • Floating point opens a menu: Half (16 bits), Single (32 bits), Double (64 bits), or any number of Exponent bits (IEEE 754 format).
  • Decimal works for vectors of any width.
  • A VHDL-2008 sfixed or ufixed signal starts in fixed point, with the binary point its index range gives; a float_pkg floating-point signal starts in floating point.

Opening records, arrays and vectors

A record or array row opens into a row per field or element, and a vector into a row per bit: click the arrow before its name, double-click the name, or right-click it and choose Open (Close closes it again). Records and arrays inside open in turn. An open row moves and is removed with its parts. An array with more than 4,096 elements shows as a whole and does not open.

Action Mouse Keyboard
Zoom around a point Ctrl+wheel, or a trackpad pinch, over the waves + (or =) / −, around the cursor, or the middle if the cursor is off-screen
Show the whole recording F
Pan in time Drag the waves; Shift+wheel; a horizontal trackpad scroll ← / →
Scroll through the rows Wheel; drag the waves up or down
Place the cursor Click in the waves
Place the second cursor Shift+click in the waves
Move the cursor to the selected signal's previous or next change Ctrl+← / Ctrl+→
Move the cursor to the previous or next marker (or second cursor) Alt+← / Alt+→

The keys act while no text field (such as the filter box) has the keyboard. The toolbar has buttons for zooming and for moving between changes, and the View and Signals menus have the same commands.

A click in the waves also selects the row under the pointer, and places the cursor on that signal's nearest change if one is within a few pixels, so that measuring from edge to edge is exact.

Cursors, markers and measuring

The cursor (A) is placed by a click. The Value column shows every row's value at the cursor, and the status bar shows its time.

The second cursor (B, dashed) is placed by Shift+click. It is labelled with its time on the axis. The time from one cursor to the other, and the frequency it makes, show under the time axis between them, and in the status bar: A 1.2 µs · B 1.21 µs · Δ 10 ns (100.0000 MHz).

Markers are named points in time, drawn in orange:

  • M (or Signals → Add a marker at the cursor) adds one at the cursor, named M1, M2 and so on.
  • Each marker's flag on the time axis shows its name and its distance from the cursor, such as M1 · +30 ns; hover over it for its time.
  • Drag a flag to move the marker; it snaps to a change of the selected signal nearby.
  • Right-click a flag to rename the marker (type in the Name field), to bring the cursor there (Cursor here), or to remove it (Remove marker). Double-click a flag to bring the cursor there.
  • Alt+← and Alt+→ move the cursor to the previous or next marker or second cursor.
  • Signals → Remove the markers and the second cursor removes them all.

Under the time axis, a line joins each mark (the cursor, the second cursor, each marker) to the next, with the time between them on it, Δ 30 ns; between the two cursors, with the frequency too. A chain of markers thus measures every interval at once: set markers on successive edges and read the periods off the axis.

The second cursor and the markers are saved in wave setups.

Finding a value

/ (or Signals → Find a value of the selected signal) opens a find bar for the selected row. Type a value and press Enter or F3: the cursor moves to the next change of that signal to the value, after the cursor. Shift+Enter or Shift+F3 finds the previous one, before the cursor. Escape closes the bar.

What you type is read this way:

You type It finds
A number, such as A5, 1010, -3 That number, read in the row's radix: hexadecimal, binary, decimal
0x1F, 0b1010, 0o17, or x"1F", b"1010", o"17" That number, in the radix it names
Digits with ?, such as 1??0 Any value that matches, ? standing for any digit
X (or U) The next unknown value: any U, X, W or -
Z The next high-impedance value
1 or 0 on a one-bit signal The next rising or falling edge
Anything else: IDLE, a string, a real A value shown as that text, ignoring case, spaces and _

When there is no such change, the bar says so: No change to A5 after 1.2 µs. Searching a very large recording goes on in the background, with its progress in the bar, and skips the parts of the recording whose values cannot match without reading them.

Analog rows

A real, or a vector read as a number in its radix (unsigned or signed decimal, fixed or floating point, Gray, BCD), can be drawn as a line: right-click its name and choose Analog:

  • Steps: the value holds until the next change;
  • Lines between changes: straight lines from one value to the next;
  • Off (digital): back to the normal row.

An analog row is taller (3 rows to start with); set its Height (rows), from 1 to 12, in the same menu. Its Range is The whole signal's (the default), What is shown (it follows what is in view), or Fixed, from and to values you set. The range shows at the left of the row.

Where many changes fall within one pixel, the line shows their range there as a bar. Values with no number (a vector with unknown bits) are shaded. Zoomed out on a large recording, the range of each part comes from the recording's summaries, without reading the changes, exact to within a pixel.

Expression rows

A row can show an expression of other signals, computed from the recording: a handshake (valid and ready), a condition (count = 10), a field of a bus (addr(7 downto 4)), a sum. Press E (or Signals → Add an expression of signals), type the expression in the bar that opens over the waves, and press Enter (or Add). The new row goes after the selected row, or at the end if none is selected.

The bar starts with the selected signal's name, and says as you type what the expression gives (1 bits, 8 bits, a real) or what is wrong with it, such as no signal is named vaild. Escape closes it.

Expressions are written as VHDL writes them:

Write Meaning
valid, count A signal, by its name
{/tb/dut/count[7:0]} A signal by its path, in braces: needed when several signals have the name (the bar says so)
'1', 'X', "0101", x"A5", o"17", b"01", 12, 0.5 Literals: bits, bit strings, whole numbers, reals
not, and, or, xor, nand, nor, xnor Element by element, with std_logic's rules: '0' and 'X' is '0'
=, /=, <, <=, >, >= Compare numbers (unsigned); the result is one bit
+, - Add or subtract unsigned numbers; the result is as wide as the wider operand
& Join vectors
x(3), x(7 downto 4), x[7:4] Elements of a vector, counted from the right, 0 being the rightmost
( ) Grouping

An operand with a bit that is not 0 or 1 makes arithmetic and comparisons unknown (X). Vectors of different widths are widened with zeros on the left. Reals compare, add and subtract as reals.

An expression row works like a signal's row: it has a radix, a color and an analog drawing, and you can find a value in it and jump between its changes. Right-click it and choose Change the expression… to edit it. Expressions are saved in wave setups and computed again for each run.

Groups

Pick rows (Ctrl+click, Shift+click), right-click one of them and choose Group the picked rows; with one row selected, Put in a group. The rows, with their parts, go into a new group where the first of them was, named Group 1 and so on.

  • Click the group's arrow to close or open it. A closed group shows how many rows it holds.
  • Right-click the group to rename it (type in the Name field) or to Ungroup it.
  • A group moves and is removed with its rows.

Groups are saved in wave setups, closed ones too.

Dividers

A divider is a row with a line across the waves and a label, to separate groups of signals. Ctrl+D (or Signals → Insert divider above the selected signal, or right-click → Insert divider above) inserts one above the selected row. Right-click the divider and type its text in the Label field.

Colors and paths

Right-click a signal's name:

  • Color: Default (the theme's), or red, orange, yellow, green, cyan, blue, purple, pink, white or gray, for its wave.
  • Copy path: copies the signal's full path, such as /tb/dut/count[7:0], for a wave setup, a script or a message.

Wave setup files

A wave setup is what the wave view shows: the signals in their order with their radix, color and analog drawing, opened records and vectors, expression rows, groups and dividers, the cursors, the markers and the visible time span. Saved as a .wavesetup file, it can be reused across runs, projects and designs, and kept in version control next to a testbench.

  • File → Save wave setup… (Ctrl+S, while the editor does not have the keyboard) saves the view.
  • File → Open wave setup… (Ctrl+Shift+O), or dropping a .wavesetup file on the window, applies one to the waveform shown.
  • jhdlw run.jhw --setup uart.wavesetup opens a waveform with a setup.
  • A project lists its setups (wave-setup lines): the first is shown when a run's waveform opens, and the Project tab lists them all; double-click one to show it (Projects).

A signal that the waveform does not have is kept in the view and shown as missing, not dropped. So one setup can serve several designs, and survives a renamed signal until you fix it.

The format

A wave setup is a text file of lines, which you can also write or change by hand. Blank lines and lines that start with # are comments. The first other line is the header, with the format's version:

jhdl-wave-setup 3

Then one statement per line, words separated by spaces:

Statement Meaning
signal PATH [radix=R] [color=C] [bit=K] [depth=D] [expanded] A row: the signal with full path PATH, as Copy path gives it (/tb/dut/count[7:0]), or a record or array by its path (/tb/bus). R is the radix (below; hex by default). C is red, orange, yellow, green, cyan, blue, purple, pink, white, gray or #rrggbb; without it, the theme's color. bit=K makes the row one bit of the vector, counted from the left (0 is the leftmost). depth=D is how deep the row is inside opened rows or groups above it; expanded marks an opened row, whose parts are the rows after it with a larger depth.
... analog=MODE [range=R] [height=H] Options of a signal line: the row is drawn as an analog line, MODE being step or linear; R is view (what is shown) or MIN:MAX (fixed), and without it the whole signal's range; H is the row's height in rows, 1 to 12.
expr "TEXT" [radix=R] [color=C] [depth=D] [analog=MODE] [range=R] [height=H] An expression row: the value of the expression TEXT (Expression rows), in double quotes (\" for a quote, \\ for a backslash), with a signal's options. A version of JanusHDL that does not know expr skips the line.
divider [depth=D] [LABEL] A divider with LABEL (the rest of the line, which may be empty).
group "NAME" [depth=D] [closed] A group: a named row holding the rows after it with a larger depth. The name is in double quotes (\" for a quote, \\ for a backslash). closed: the group is closed.
cursor N fs The cursor at N femtoseconds.
cursor2 N fs The second cursor.
marker N fs NAME A marker at N femtoseconds, named NAME (the rest of the line).
view A fs B fs The visible span, from A to B femtoseconds.

Times are in femtoseconds, so that a setup does not depend on a waveform's time resolution.

The radixes:

R Shows a vector as
binary Its bits
octal A digit per 3 bits
hex A digit per 4 bits
unsigned, signed A decimal number (signed: two's complement)
ascii A character per 8 bits, in quotes
sfixed:F, ufixed:F A fixed-point number with F fraction bits, signed or not
float:E An IEEE 754 floating-point number with E exponent bits (5 half, 8 single, 11 double)
gray The number a Gray code encodes
bcd A decimal digit per 4 bits

An example:

jhdl-wave-setup 3
# UART transmit path
signal /tb/clk radix=binary
divider Transmitter
signal /tb/dut/tx_data[7:0] radix=hex color=cyan
signal /tb/dut/tx_busy color=#f59e3d
group "Receiver"
signal /tb/dut/rx_data[7:0] depth=1 radix=ascii
signal /tb/dut/rx_level depth=1 analog=step height=4
expr "tx_busy and not rx_busy" color=green
cursor 1500000000 fs
marker 2000000000 fs first byte
view 0 fs 20000000000 fs

A setup is saved in the oldest version that holds what it uses, so that older versions of JanusHDL can read it: version 1 when it uses only the radixes binary, hex, unsigned and signed; version 2 for another radix; version 3 when it has a second cursor, a marker, an analog row or a group. Later versions may add statements and options: a version of JanusHDL skips those it does not know and shows what it can; it refuses a header version it does not read.

Saving the waves as an image

File → Save the waves as an image (PNG or SVG)… saves the wave view as you see it, for a report, a review or a bug report. The file's name chooses the format:

  • PNG (waves.png, the default): a picture of the view, at your screen's resolution.
  • SVG (a name ending in .svg): a drawing that stays sharp at any size, and whose text can be selected and searched.

The names, values, waves, cursors and markers are in the image; in an SVG drawing, the selection and the find bars are left out.

Very large recordings

Waveforms of any size open. A waveform database is read a block at a time, as the view needs it: opening reads its index, not its changes. However long the run, the viewer keeps at most 512 MB of the recording in memory, and reads again what it dropped when it is shown again. Zoomed out, it draws from summaries of the blocks kept in the file, without reading the changes, and shows what it would show had it read them all.

Measured: a recording of 1.8 billion value changes (a 5.2 GB file) opened in under 4 seconds, in less than 1 GB of memory.

A VCD file is read into a temporary waveform database as it loads (in your temporary folder; removed when the file is closed), so a large VCD file opens in little memory too. Rows of bits and records of a large recording are built over several frames, so the window stays responsive.

If part of a waveform database cannot be read (a damaged disk, say), those changes show as unknown values, and the status bar says some changes could not be read.

VCD files

The viewer reads 4-state VCD files as IEEE 1364 and IEEE 1800 define them, plus the nine std_ulogic values (U X 0 1 Z W L H -) that VHDL simulators write, and the text and real values jhdl --vcd writes.

  • When several $var declarations share one identifier (a port and the signal connected to it), they are one signal shown under several names.
  • A file with an error is refused with what is wrong and on which line, for example: line 42: value change for unknown identifier %: it was not declared with $var in the header.
  • Extended VCD files (written by $dumpports, with port directions and strengths) are not supported yet; the viewer says so.

A VCD file holds values only: the insight panels, which need the design's structure and delta cycles, say so. Record with jhdl --wave to have them.