Skip to content

Commit a838a3b

Browse files
authored
RFC #74: Structured VCD Output
2 parents 0860eb1 + 2af7f26 commit a838a3b

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

text/0074-structured-vcd.md

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
- Start Date: 2025-05-12
2+
- RFC PR: [amaranth-lang/rfcs#0074](https://github.com/amaranth-lang/rfcs/pull/0074)
3+
- Amaranth Issue: [amaranth-lang/amaranth#1599](https://github.com/amaranth-lang/amaranth/issues/1599)
4+
5+
# Structured VCD Output
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
When generating VCD files, use the `scope` keyword to distinguish aggregate signals.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
These changes are intended to make it easier for waveform viewers to recover information about aggregate signals from VCD files.
16+
This is partially motivated by an issue with VCD parsing that occurs in Surfer (see [ekiwi/wellen#36](https://github.com/ekiwi/wellen/issues/36)).
17+
Ideally, this leads to a better user experience when inspecting simulated Amaranth designs with a waveform viewer.
18+
19+
## Guide-level explanation
20+
[guide-level-explanation]: #guide-level-explanation
21+
22+
This RFC describes changes to the way Amaranth generates a VCD file.
23+
When reading a VCD file, waveform viewers like [Surfer](https://surfer-project.org/) and [GTKWave](https://gtkwave.sourceforge.net/) support use of the `scope` keyword for organizing variables into groups.
24+
Currently, when writing a VCD file, Amaranth uses the `module` scope to distinguish between signals belonging to different modules.
25+
For instance:
26+
27+
```
28+
$scope module top $end # top
29+
$var wire 1 <id> clk $end # top.clk
30+
$var wire 1 <id> rst $end # top.rst
31+
$var wire 32 <id> in $end # top.in
32+
$var wire 32 <id> out $end # top.out
33+
$scope module submodule $end # top.submodule
34+
$var wire 1 <id> clk $end # top.submodule.clk
35+
$var wire 1 <id> rst $end # top.submodule.rst
36+
$var wire 5 <id> in $end # top.submodule.in
37+
$var wire 5 <id> out $end # top.submodule.out
38+
$upscope $end
39+
$upscope $end
40+
```
41+
42+
However, it does *not* attempt to use scopes for organizing the members of aggregate signals with array-like or struct-like datatypes.
43+
Instead, when creating VCD variables for aggregate signals, members are distinguished only by appending to the name of the parent signal.
44+
For instance, a signal `top.submodule.my_array` with the type `ArrayLayout(unsigned(32), 4)` is currently represented as:
45+
46+
```
47+
$scope module top $end # top
48+
...
49+
$scope module submodule $end # top.submodule
50+
...
51+
$var wire 128 <id> my_array $end # top.submodule.my_array
52+
$var wire 32 <id> my_array[0] $end # top.submodule.my_array[0]
53+
$var wire 32 <id> my_array[1] $end # top.submodule.my_array[1]
54+
$var wire 32 <id> my_array[2] $end # top.submodule.my_array[2]
55+
$var wire 32 <id> my_array[3] $end # top.submodule.my_array[3]
56+
$upscope $end
57+
$upscope $end
58+
```
59+
60+
This is not sufficient for explicitly conveying the relationship between aggregate signals and their members to a waveform viewer.
61+
In this case, `my_array` does not explicitly *contain* its members, and waveform viewers may only *infer* this relationship by attempting to recover it from the names.
62+
63+
This RFC proposes the use of `scope vhdl_array` and `scope vhdl_record` to pass information about these relationships to a waveform viewer.
64+
After this change, the VCD output above would become:
65+
66+
```
67+
$scope module top $end # top
68+
...
69+
$scope module submodule $end # top.submodule
70+
...
71+
$comment Flattened representation of 'my_array' $end
72+
$var wire 128 <id> my_array $end # top.submodule.my_array
73+
74+
$comment Hierarchical representation of 'my_array' members $end
75+
$scope vhdl_array my_array $end # top.submodule.my_array
76+
$var wire 32 <id> 0 $end # top.submodule.my_array.0
77+
$var wire 32 <id> 1 $end # top.submodule.my_array.1
78+
$var wire 32 <id> 2 $end # top.submodule.my_array.2
79+
$var wire 32 <id> 3 $end # top.submodule.my_array.3
80+
$upscope $end
81+
82+
$upscope $end
83+
$upscope $end
84+
```
85+
86+
These images demonstrate the difference in GTKWave output:
87+
88+
<figure>
89+
<img align="center" src="./0074-structured-vcd/gtkwave_pre_rfc.png" alt="a screenshot of array-like signals in GTKWave (current behavior)">
90+
<figcaption><i>Figure 1. A screenshot of array-like signals in GTKWave (current behavior)</i></figcaption>
91+
</figure>
92+
93+
<figure>
94+
<img align="center" src="./0074-structured-vcd/gtkwave_post_rfc.png" alt="a screenshot of array-like signals in GTKWave (proposed behavior)">
95+
<figcaption><i>Figure 2. A screenshot of array-like signals in GTKWave (proposed behavior)</i></figcaption>
96+
</figure>
97+
98+
... and the difference in Surfer output:
99+
100+
<figure>
101+
<img align="center" src="./0074-structured-vcd/surfer_pre_rfc.png" alt="a screenshot of array-like signals in Surfer (current behavior)">
102+
<figcaption><i>Figure 3. A screenshot of array-like signals in Surfer (current behavior)</i></figcaption>
103+
</figure>
104+
105+
<figure>
106+
<img align="center" src="./0074-structured-vcd/surfer_post_rfc.png" alt="a screenshot of array-like signals in Surfer (proposed behavior)">
107+
<figcaption><i>Figure 4. A screenshot of array-like signals in Surfer (proposed behavior)</i></figcaption>
108+
</figure>
109+
110+
## Reference-level explanation
111+
[reference-level-explanation]: #reference-level-explanation
112+
113+
Currently, Amaranth represents aggregate signals in the VCD by appending the names of elements/members to the name of the signal.
114+
When simulator output is written to a VCD file, we intend that signals with aggregate datatypes are explicitly given their own scope and split into multiple VCD variables.
115+
116+
### Changes to VCD Writing
117+
118+
We propose the following conventions:
119+
120+
- The `module` scope defines a group of signals belonging to the same `Module`
121+
- The `vhdl_array` scope defines a group of signals belonging to an array (such as a signal with `ArrayLayout`)
122+
- The `vhdl_record` scope defines a structured group of signals (such as a signal with `StructLayout`, `UnionLayout`, or `FlexibleLayout`)
123+
124+
After these changes, this functionality will be enabled by default in the VCD writer.
125+
These changes will **not** be backported to Amaranth 0.5.x.
126+
Some users may require compatibility with output that is closer to the actual VCD specification.
127+
In this case, users that need the pre-RFC behavior are expected to either:
128+
129+
- Opt-out on a case-by-case basis by passing arguments to `write_vcd()` (ie. `pure=True`)
130+
- Opt-out globally by setting an environment variable (ie. `AMARANTH_PURE_VCD=1`)
131+
132+
### Changes to `pyvcd`
133+
134+
The VCD writer in Amaranth depends on `pyvcd`, which represents VCD scopes with a `ScopeType` enum.
135+
`vhdl_record` and `vhdl_array` variants are required to implement this RFC.
136+
137+
### Changes to Amaranth Playground
138+
139+
The Amaranth Playground has a waveform viewer that depends on the [`d3-wave`](https://github.com/Nic30/d3-wave) library.
140+
`d3-wave` has an exported/extensible `RowRendererBits` class that can be used to implement renderers for particular datatypes.
141+
Custom renderers for aggregate datatypes are required for rendering grouped signals in the Amaranth Playground.
142+
143+
### Expected VCD Output: Array-like
144+
145+
```python
146+
from amaranth import *
147+
from amaranth.lib.data import *
148+
149+
foo = Signal(ArrayLayout(unsigned(32), 4))
150+
```
151+
152+
For `ArrayLayout`, each element in the array should become a VCD variable whose name is integer array index.
153+
In this case, the signal `foo` is split into `foo.0`, `foo.1`, `foo.2`, and `foo.3`:
154+
155+
```
156+
$scope vhdl_array foo $end
157+
$var wire 32 <id> 0 $end
158+
$var wire 32 <id> 1 $end
159+
$var wire 32 <id> 2 $end
160+
$var wire 32 <id> 3 $end
161+
$upscope $end
162+
```
163+
164+
### Expected VCD Output: Struct-like
165+
166+
```python
167+
from amaranth import *
168+
from amaranth.lib.data import *
169+
170+
foo = Signal(StructLayout({
171+
"a": unsigned(1),
172+
"b": unsigned(4),
173+
"c": unsigned(32),
174+
}))
175+
```
176+
177+
For `StructLayout`, each member should become a VCD variable with the member name.
178+
In this case, the signal `bar` is split into `bar.a`, `bar.b`, and `bar.c`:
179+
180+
```
181+
$scope vhdl_record bar $end
182+
$var wire 1 id a $end
183+
$var wire 4 id b $end
184+
$var wire 32 id c $end
185+
$upscope $end
186+
```
187+
188+
### Expected VCD Output: Nested Aggregates
189+
190+
```python
191+
from amaranth import *
192+
from amaranth.lib.data import *
193+
194+
class MyStruct(Struct):
195+
a: unsigned(1)
196+
b: unsigned(4)
197+
c: ArrayLayout(unsigned(32), 4),
198+
199+
# A struct-like signal
200+
foo = Signal(MyStruct)
201+
202+
```
203+
204+
For signals with nested aggregate types, the scopes are nested in the same way that `scope module` is already used for nested modules.
205+
In this case, the signal `foo` is split like this:
206+
207+
```
208+
$scope vhdl_record bar $end
209+
$var wire 1 id a $end
210+
$var wire 4 id b $end
211+
$scope vhdl_array c $end
212+
$var wire 32 id 0 $end
213+
$var wire 32 id 1 $end
214+
$var wire 32 id 2 $end
215+
$var wire 32 id 3 $end
216+
$upscope $end
217+
$upscope $end
218+
```
219+
220+
## Drawbacks
221+
[drawbacks]: #drawbacks
222+
223+
- VCD is not a particularly efficient format, and this adds even more bytes to generated VCD files.
224+
225+
- This breaks any existing compatibility with waveform viewers that do not handle the `vhdl_record` and `vhdl_array` scope types.
226+
Since these are not part of the VCD specification, some waveform viewers may fail to handle this.
227+
228+
## Rationale and alternatives
229+
[rationale-and-alternatives]: #rationale-and-alternatives
230+
231+
- The choice of `scope vhdl_array` and `scope vhdl_record` are suggested due to known compatibility with Surfer and GTKWave.
232+
However, note that these are not part of the VCD specification (which is somewhat old and under-defined).
233+
234+
### Alternatives
235+
236+
- Expose an environment variable (or a parameter in `write_vcd()`) allowing users to opt-in/opt-out of structured VCD output
237+
- Include support for a different waveform format that has better-defined support for variables with composite datatypes.
238+
239+
## Prior art
240+
[prior-art]: #prior-art
241+
242+
- As far as I can tell, use of the `vhdl_record` and `vhdl_array` scope types comes from the [ghdl/ghdl](https://github.com/ghdl/ghdl) simulator.
243+
- [verilator/verilator](https://github.com/verilator/verilator) makes no attempt to use these scope types
244+
- [steveicarus/iverilog](https://github.com/steveicarus/iverilog) makes no attempt to use these scopes types
245+
246+
## Unresolved questions
247+
[unresolved-questions]: #unresolved-questions
248+
249+
None.
250+
251+
## Future possibilities
252+
[future-possibilities]: #future-possibilities
253+
254+
Since this adds more overhead to VCD output, it's worth mentioning that VCD may be unsuitable for testing very large designs.
255+
This RFC can also serve as a stepping stone for supporting alternative waveform formats in the future.
256+
51.7 KB
Loading
62.9 KB
Loading
31.9 KB
Loading
36.7 KB
Loading

0 commit comments

Comments
 (0)