Simulator is writing multiple transition of the same signal at a same timestamp
question
# Description
Amaranth simulator is generating in some cases multiple transition of a signal at one timestamp of a vcd file. I observed that because it generate a visual spike with vaporview (I reported that issue).
When I export the amaranth class to verilog and run the same test bench with icarus verilog I do not have thoses multiple transition of a signal at one timestamp.
(On the left icarus verilog siulator result; on the right amaranth simulator result; consider the two last signals)
<img width="2180" height="474" alt="Image" src="https://github.com/user-attachments/assets/0a88b377-e9e2-4208-9440-89e84e7d804a" />
I tried to reproduce the bug on smaller example but wasn't able to. So I join the code that was causing this issue in our course. I am wondering what could be causing that issue.
# Expected behavior
Reading the _"IEEE Standard Verilog® Hardware Description Language, year 2001"_ it seems the case of multiple transition of the same signal at one timestamp is not specified (neither allowed nor forbidden).
I would expect to take in consideration the transition to the last occurrence in the timestamp, for #1 it would be 1!. (We could even consider previous state was 0, and double transition to X and then 1 at timestamp #1, then we should consider 0->1 transition).
In my opinion a vcd file should not contains such pattern.
# Configuration
`Dockerfile`:
```Dockerfile
FROM debian:bullseye
WORKDIR /home
RUN apt-get -y update &&\
apt-get install -y python3 python3-distutils curl &&\
curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py &&\
python3 get-pip.py &&\
python3 -m pip install --upgrade 'amaranth[builtin-yosys]' numpy
ENV IVERILOG_VERSION=v12-branch
RUN apt-get -y update && \
apt-get install -y \
automake \
autoconf \
gperf \
build-essential \
flex \
bison \
git && \
rm -rf /var/lib/apt/lists/*
RUN git clone --branch=${IVERILOG_VERSION} https://github.com/steveicarus/iverilog && \
cd iverilog && \
bash autoconf.sh && \
./configure && \
make && \
make install && \
cd && \
rm -rf iverilog
CMD ["/bin/bash"]
```
`pip freeze`
```pip
amaranth==0.5.8
amaranth-yosys==0.50.0.0.post122
Jinja2==3.1.6
jschon==0.11.1
MarkupSafe==3.0.3
numpy==2.0.2
packaging==26.0
pyvcd==0.4.1
rfc3986==2.0.0
wasmtime==43.0.0
```
# Files
`main.py`
```python
from amaranth import *
def is_power_of_two(x):
return (x & (x - 1)) == 0
class AdderTree(Elaboratable):
def __init__(self, acc_bits, fan_in, signed=True):
self.acc_bits = acc_bits
self.fan_in = fan_in
self.signed = signed
assert is_power_of_two(fan_in)
assert fan_in >= 2
self.in_data = Array([
Signal(Shape(acc_bits, signed=signed), name=f'in_data_{fan_in}_{i}')
for i in range(fan_in)])
self.in_ovf = Array([Signal(1, name=f'in_ovf_{fan_in}_{i}')
for i in range(fan_in)])
self.in_valid = Array([Signal(1, name=f'in_valid_{fan_in}_{i}')
for i in range(fan_in)])
self.out_d = Signal(Shape(acc_bits, signed=signed), name=f"out_d_{fan_in}")
self.out_ovf = Signal(1, name=f"out_ovf_{fan_in}")
self.out_valid = Signal(1, name=f"out_valid_{fan_in}")
if fan_in > 2:
self.tree_l = AdderTree(acc_bits, fan_in // 2, signed)
self.tree_r = AdderTree(acc_bits, fan_in // 2, signed)
def elaborate(self, platform):
m = Module()
if self.fan_in > 2:
m.submodules.tree_l = tree_l = self.tree_l
m.submodules.tree_r = tree_r = self.tree_r
half = self.fan_in // 2
for i in range(half):
m.d.comb += [
tree_l.in_data[i].eq(self.in_data[i]),
tree_l.in_ovf[i].eq(self.in_ovf[i]),
tree_l.in_valid[i].eq(self.in_valid[i]),
]
for i in range(half):
m.d.comb += [
tree_r.in_data[i].eq(self.in_data[half + i]),
tree_r.in_ovf[i].eq(self.in_ovf[half + i]),
tree_r.in_valid[i].eq(self.in_valid[half + i]),
]
sum_result = Signal(Shape(self.acc_bits + 1, signed=self.signed))
m.d.comb += [
sum_result.eq(tree_l.out_d + tree_r.out_d),
self.out_d.eq(sum_result[:self.acc_bits]),
self.out_valid.eq(tree_l.out_valid & tree_r.out_valid),
]
# Overflow detection
if self.signed:
# Signed overflow: sign bit mismatch after truncation
m.d.comb += self.out_ovf.eq(
tree_l.out_ovf | tree_r.out_ovf |
(sum_result[self.acc_bits] != sum_result[self.acc_bits - 1])
)
else:
m.d.comb += self.out_ovf.eq(
tree_l.out_ovf | tree_r.out_ovf | sum_result[self.acc_bits]
)
else:
# Base case: fan_in == 2
sum_result = Signal(Shape(self.acc_bits + 1, signed=self.signed))
m.d.comb += [
sum_result.eq(self.in_data[0] + self.in_data[1]),
self.out_d.eq(sum_result[:self.acc_bits]),
self.out_valid.eq(self.in_valid[0] & self.in_valid[1]),
]
if self.signed:
m.d.comb += self.out_ovf.eq(
self.in_ovf[0] | self.in_ovf[1] |
(sum_result[self.acc_bits] != sum_result[self.acc_bits - 1])
)
else:
m.d.comb += self.out_ovf.eq(
self.in_ovf[0] | self.in_ovf[1] | sum_result[self.acc_bits]
)
return m
if __name__ == '__main__':
acc_bits = 8
fan_in = 4
signed = True
dut = AdderTree(acc_bits=acc_bits, fan_in=fan_in, signed=signed)
from amaranth.sim import Simulator, Settle, Delay
import numpy as np
np.random.seed(42)
adder_tree_tb_template = open("adder_tree_tb_template.v", "r").read()
tb_def = ""
for i in range(fan_in):
tb_def += f"reg [7:0] in_data_{fan_in}_{i};\n"
tb_def += f"reg in_ovf_{fan_in}_{i};\n"
tb_def += f"reg in_valid_{fan_in}_{i};\n"
tb_def += f"wire [7:0] out_d_{fan_in};\n"
tb_def += f"wire out_ovf_{fan_in};\n"
tb_def += f"wire out_valid_{fan_in};\n"
port = ""
for i in range(fan_in):
port += f".in_data_{fan_in}_{i}(in_data_{fan_in}_{i}),\n"
port += f".in_ovf_{fan_in}_{i}(in_ovf_{fan_in}_{i}),\n"
port += f".in_valid_{fan_in}_{i}(in_valid_{fan_in}_{i}),\n"
port += f".out_d_{fan_in}(out_d_{fan_in}),\n"
port += f".out_ovf_{fan_in}(out_ovf_{fan_in}),\n"
port += f".out_valid_{fan_in}(out_valid_{fan_in})\n"
async def test_case(ctx, dut, in_data, in_ovf, in_valid, tb):
tb[0] += "#1\n"
for i in range(fan_in):
tb[0] += f"in_data_{fan_in}_{i} = {in_data[i]};\n"
tb[0] += f"in_ovf_{fan_in}_{i} = {in_ovf[i]};\n"
tb[0] += f"in_valid_{fan_in}_{i} = {in_valid[i]};\n"
for i, d in enumerate(in_data):
ctx.set(dut.in_data[i],int(d))
for i, d in enumerate(in_ovf):
ctx.set(dut.in_ovf[i],bool(d))
for i, d in enumerate(in_valid):
ctx.set(dut.in_valid[i],bool(d))
await ctx.delay(1e-6)
hw_sum = ctx.get(dut.out_d)
hw_ovf = ctx.get(dut.out_ovf)
if hw_ovf:
assert ((hw_sum - sum(in_data)) % (2 ** acc_bits) == 0)
else:
assert (hw_sum == sum(in_data))
async def bench(ctx):
tb = [""]
for _ in range(512):
await test_case(ctx, dut,
in_data=np.random.randint(
low=-2**(acc_bits-2),
high=2**(acc_bits-2), size=fan_in),
in_ovf=np.random.randint(
low=0, high=1, size=fan_in),
in_valid=np.random.randint(
low=0, high=2, size=fan_in),
tb=tb
)
with open("data_for_tb.v", "w") as file_tb:
file_tb.write(adder_tree_tb_template.format(dutdef=tb_def, port=port, tb=tb[0]))
from pathlib import Path
p = Path(__file__)
sim = Simulator(dut)
sim.add_testbench(bench)
with open(p.with_suffix('.vcd'), 'w') as f:
with sim.write_vcd(f):
sim.run()
from amaranth.back import verilog
# top = AdderTree(acc_bits=acc_bits, fan_in=fan_in, signed=signed)
top = dut
with open(p.with_suffix('.v'), 'w') as f:
f.write(
verilog.convert(
top,
ports=[*top.in_data, *top.in_ovf, *top.in_valid,
top.out_d, top.out_ovf, top.out_valid]))
```
`adder_tree_tb_template.v`
```verilog
`timescale 1us / 1fs
`include "adder_tree.v"
module adder_tree_tb;
{dutdef}
top dut (
{port}
);
initial begin
$dumpfile("adder_tree_verilog.vcd");
$dumpvars(0, adder_tree_tb);
{tb}
$finish;
end
endmodule
```
3 条评论