back.pysim: redesign the simulator.

The redesign introduces no fundamental incompatibilities, but it does
involve minor breaking changes:
  * The simulator commands were moved from hdl.ast to back.pysim
    (instead of only being reexported from back.pysim).
  * back.pysim.DeadlineError was removed.

Summary of changes:
  * The new simulator compiles HDL to Python code and is >6x faster.
    (The old one compiled HDL to lots of Python lambdas.)
  * The new simulator is a straightforward, rigorous implementation
    of the Synchronous Reactive Programming paradigm, instead of
    a pile of ad-hoc code with no particular design driving it.
  * The new simulator never raises DeadlineError, and there is no
    limit on the amount of delta cycles.
  * The new simulator robustly handles multiclock designs.
  * The new simulator can be reset, such that the compiled design
    can be reused, which can save significant runtime with large
    designs.
  * Generators can no longer be added as processes, since that would
    break reset(); only generator functions may be. If necessary,
    they may be added by wrapping them into a generator function;
    a deprecated fallback does just that. This workaround will raise
    an exception if the simulator is reset and restarted.
  * The new simulator does not depend on Python extensions.
    (The old one required bitarray, which did not provide wheels.)

Fixes #28.
Fixes #34.
Fixes #160.
Fixes #161.
Fixes #215.
Fixes #242.
Fixes #262.
This commit is contained in:
whitequark 2019-11-22 08:32:41 +00:00
parent f8428ff505
commit 7df70059d1
7 changed files with 1164 additions and 906 deletions

View file

@ -19,10 +19,7 @@ ctr = Counter(width=16)
print(verilog.convert(ctr, ports=[ctr.o, ctr.en])) print(verilog.convert(ctr, ports=[ctr.o, ctr.en]))
with pysim.Simulator(ctr, sim = pysim.Simulator(ctr)
vcd_file=open("ctrl.vcd", "w"),
gtkw_file=open("ctrl.gtkw", "w"),
traces=[ctr.en, ctr.v, ctr.o]) as sim:
sim.add_clock(1e-6) sim.add_clock(1e-6)
def ce_proc(): def ce_proc():
yield; yield; yield yield; yield; yield
@ -31,5 +28,6 @@ with pysim.Simulator(ctr,
yield ctr.en.eq(0) yield ctr.en.eq(0)
yield; yield; yield yield; yield; yield
yield ctr.en.eq(1) yield ctr.en.eq(1)
sim.add_sync_process(ce_proc()) sim.add_sync_process(ce_proc)
with sim.write_vcd("ctrl.vcd", "ctrl.gtkw", traces=[ctr.en, ctr.v, ctr.o]):
sim.run_until(100e-6, run_passive=True) sim.run_until(100e-6, run_passive=True)

File diff suppressed because it is too large Load diff

View file

@ -21,15 +21,24 @@ def run_simulation(fragment_or_module, generators, clocks={"sync": 10}, vcd_name
generators = {"sync": generators} generators = {"sync": generators}
fragment.domains += ClockDomain("sync") fragment.domains += ClockDomain("sync")
with Simulator(fragment, vcd_file=open(vcd_name, "w") if vcd_name else None) as sim: sim = Simulator(fragment)
for domain, period in clocks.items(): for domain, period in clocks.items():
sim.add_clock(period / 1e9, domain=domain) sim.add_clock(period / 1e9, domain=domain)
for domain, processes in generators.items(): for domain, processes in generators.items():
def wrap(process):
def wrapper():
yield from process
return wrapper
if isinstance(processes, Iterable) and not inspect.isgenerator(processes): if isinstance(processes, Iterable) and not inspect.isgenerator(processes):
for process in processes: for process in processes:
sim.add_sync_process(process, domain=domain) sim.add_sync_process(wrap(process), domain=domain)
else:
sim.add_sync_process(wrap(processes), domain=domain)
if vcd_name is not None:
with sim.write_vcd(vcd_name):
sim.run()
else: else:
sim.add_sync_process(processes, domain=domain)
sim.run() sim.run()

View file

@ -19,7 +19,8 @@ class FFSynchronizerTestCase(FHDLTestCase):
i = Signal() i = Signal()
o = Signal() o = Signal()
frag = FFSynchronizer(i, o) frag = FFSynchronizer(i, o)
with Simulator(frag) as sim:
sim = Simulator(frag)
sim.add_clock(1e-6) sim.add_clock(1e-6)
def process(): def process():
self.assertEqual((yield o), 0) self.assertEqual((yield o), 0)
@ -37,7 +38,8 @@ class FFSynchronizerTestCase(FHDLTestCase):
i = Signal(reset=1) i = Signal(reset=1)
o = Signal() o = Signal()
frag = FFSynchronizer(i, o, reset=1) frag = FFSynchronizer(i, o, reset=1)
with Simulator(frag) as sim:
sim = Simulator(frag)
sim.add_clock(1e-6) sim.add_clock(1e-6)
def process(): def process():
self.assertEqual((yield o), 1) self.assertEqual((yield o), 1)
@ -69,7 +71,7 @@ class ResetSynchronizerTestCase(FHDLTestCase):
s = Signal(reset=1) s = Signal(reset=1)
m.d.sync += s.eq(0) m.d.sync += s.eq(0)
with Simulator(m, vcd_file=open("test.vcd", "w")) as sim: sim = Simulator(m)
sim.add_clock(1e-6) sim.add_clock(1e-6)
def process(): def process():
# initial reset # initial reset
@ -84,7 +86,7 @@ class ResetSynchronizerTestCase(FHDLTestCase):
yield arst.eq(1) yield arst.eq(1)
yield Delay(1e-8) yield Delay(1e-8)
self.assertEqual((yield s), 1) self.assertEqual((yield s), 0)
yield Tick(); yield Delay(1e-8) yield Tick(); yield Delay(1e-8)
self.assertEqual((yield s), 1) self.assertEqual((yield s), 1)
yield arst.eq(0) yield arst.eq(0)
@ -96,4 +98,5 @@ class ResetSynchronizerTestCase(FHDLTestCase):
self.assertEqual((yield s), 0) self.assertEqual((yield s), 0)
yield Tick(); yield Delay(1e-8) yield Tick(); yield Delay(1e-8)
sim.add_process(process) sim.add_process(process)
with sim.write_vcd("test.vcd"):
sim.run() sim.run()

View file

@ -8,26 +8,26 @@ from ..lib.coding import *
class EncoderTestCase(FHDLTestCase): class EncoderTestCase(FHDLTestCase):
def test_basic(self): def test_basic(self):
enc = Encoder(4) enc = Encoder(4)
with Simulator(enc) as sim:
def process(): def process():
self.assertEqual((yield enc.n), 1) self.assertEqual((yield enc.n), 1)
self.assertEqual((yield enc.o), 0) self.assertEqual((yield enc.o), 0)
yield enc.i.eq(0b0001) yield enc.i.eq(0b0001)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 0) self.assertEqual((yield enc.n), 0)
self.assertEqual((yield enc.o), 0) self.assertEqual((yield enc.o), 0)
yield enc.i.eq(0b0100) yield enc.i.eq(0b0100)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 0) self.assertEqual((yield enc.n), 0)
self.assertEqual((yield enc.o), 2) self.assertEqual((yield enc.o), 2)
yield enc.i.eq(0b0110) yield enc.i.eq(0b0110)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 1) self.assertEqual((yield enc.n), 1)
self.assertEqual((yield enc.o), 0) self.assertEqual((yield enc.o), 0)
sim = Simulator(enc)
sim.add_process(process) sim.add_process(process)
sim.run() sim.run()
@ -35,26 +35,26 @@ class EncoderTestCase(FHDLTestCase):
class PriorityEncoderTestCase(FHDLTestCase): class PriorityEncoderTestCase(FHDLTestCase):
def test_basic(self): def test_basic(self):
enc = PriorityEncoder(4) enc = PriorityEncoder(4)
with Simulator(enc) as sim:
def process(): def process():
self.assertEqual((yield enc.n), 1) self.assertEqual((yield enc.n), 1)
self.assertEqual((yield enc.o), 0) self.assertEqual((yield enc.o), 0)
yield enc.i.eq(0b0001) yield enc.i.eq(0b0001)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 0) self.assertEqual((yield enc.n), 0)
self.assertEqual((yield enc.o), 0) self.assertEqual((yield enc.o), 0)
yield enc.i.eq(0b0100) yield enc.i.eq(0b0100)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 0) self.assertEqual((yield enc.n), 0)
self.assertEqual((yield enc.o), 2) self.assertEqual((yield enc.o), 2)
yield enc.i.eq(0b0110) yield enc.i.eq(0b0110)
yield Delay() yield Settle()
self.assertEqual((yield enc.n), 0) self.assertEqual((yield enc.n), 0)
self.assertEqual((yield enc.o), 1) self.assertEqual((yield enc.o), 1)
sim = Simulator(enc)
sim.add_process(process) sim.add_process(process)
sim.run() sim.run()
@ -62,22 +62,22 @@ class PriorityEncoderTestCase(FHDLTestCase):
class DecoderTestCase(FHDLTestCase): class DecoderTestCase(FHDLTestCase):
def test_basic(self): def test_basic(self):
dec = Decoder(4) dec = Decoder(4)
with Simulator(dec) as sim:
def process(): def process():
self.assertEqual((yield dec.o), 0b0001) self.assertEqual((yield dec.o), 0b0001)
yield dec.i.eq(1) yield dec.i.eq(1)
yield Delay() yield Settle()
self.assertEqual((yield dec.o), 0b0010) self.assertEqual((yield dec.o), 0b0010)
yield dec.i.eq(3) yield dec.i.eq(3)
yield Delay() yield Settle()
self.assertEqual((yield dec.o), 0b1000) self.assertEqual((yield dec.o), 0b1000)
yield dec.n.eq(1) yield dec.n.eq(1)
yield Delay() yield Settle()
self.assertEqual((yield dec.o), 0b0000) self.assertEqual((yield dec.o), 0b0000)
sim = Simulator(dec)
sim.add_process(process) sim.add_process(process)
sim.run() sim.run()

View file

@ -1,3 +1,4 @@
import os
from contextlib import contextmanager from contextlib import contextmanager
from .utils import * from .utils import *
@ -25,16 +26,14 @@ class SimulatorUnitTestCase(FHDLTestCase):
for signal in flatten(s._lhs_signals() for s in Statement.cast(stmt)): for signal in flatten(s._lhs_signals() for s in Statement.cast(stmt)):
frag.add_driver(signal) frag.add_driver(signal)
with Simulator(frag, sim = Simulator(frag)
vcd_file =open("test.vcd", "w"),
gtkw_file=open("test.gtkw", "w"),
traces=[*isigs, osig]) as sim:
def process(): def process():
for isig, input in zip(isigs, inputs): for isig, input in zip(isigs, inputs):
yield isig.eq(input) yield isig.eq(input)
yield Delay() yield Settle()
self.assertEqual((yield osig), output.value) self.assertEqual((yield osig), output.value)
sim.add_process(process) sim.add_process(process)
with sim.write_vcd("test.vcd", "test.gtkw", traces=[*isigs, osig]):
sim.run() sim.run()
def test_invert(self): def test_invert(self):
@ -213,6 +212,13 @@ class SimulatorUnitTestCase(FHDLTestCase):
stmt = lambda y, a: [Cat(l, m, n).eq(a), y.eq(Cat(n, m, l))] stmt = lambda y, a: [Cat(l, m, n).eq(a), y.eq(Cat(n, m, l))]
self.assertStatement(stmt, [C(0b100101110, 9)], C(0b110101100, 9)) self.assertStatement(stmt, [C(0b100101110, 9)], C(0b110101100, 9))
def test_nested_cat_lhs(self):
l = Signal(3)
m = Signal(3)
n = Signal(3)
stmt = lambda y, a: [Cat(Cat(l, Cat(m)), n).eq(a), y.eq(Cat(n, m, l))]
self.assertStatement(stmt, [C(0b100101110, 9)], C(0b110101100, 9))
def test_record(self): def test_record(self):
rec = Record([ rec = Record([
("l", 1), ("l", 1),
@ -277,8 +283,9 @@ class SimulatorUnitTestCase(FHDLTestCase):
class SimulatorIntegrationTestCase(FHDLTestCase): class SimulatorIntegrationTestCase(FHDLTestCase):
@contextmanager @contextmanager
def assertSimulation(self, module, deadline=None): def assertSimulation(self, module, deadline=None):
with Simulator(module) as sim: sim = Simulator(module)
yield sim yield sim
with sim.write_vcd("test.vcd", "test.gtkw"):
if deadline is None: if deadline is None:
sim.run() sim.run()
else: else:
@ -300,11 +307,15 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
yield Delay(1e-6) yield Delay(1e-6)
self.assertEqual((yield self.count), 4) self.assertEqual((yield self.count), 4)
yield self.sync.clk.eq(1) yield self.sync.clk.eq(1)
self.assertEqual((yield self.count), 4)
yield Settle()
self.assertEqual((yield self.count), 5) self.assertEqual((yield self.count), 5)
yield Delay(1e-6) yield Delay(1e-6)
self.assertEqual((yield self.count), 5) self.assertEqual((yield self.count), 5)
yield self.sync.clk.eq(0) yield self.sync.clk.eq(0)
self.assertEqual((yield self.count), 5) self.assertEqual((yield self.count), 5)
yield Settle()
self.assertEqual((yield self.count), 5)
for _ in range(3): for _ in range(3):
yield Delay(1e-6) yield Delay(1e-6)
yield self.sync.clk.eq(1) yield self.sync.clk.eq(1)
@ -328,6 +339,26 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
self.assertEqual((yield self.count), 0) self.assertEqual((yield self.count), 0)
sim.add_sync_process(process) sim.add_sync_process(process)
def test_reset(self):
self.setUp_counter()
sim = Simulator(self.m)
sim.add_clock(1e-6)
times = 0
def process():
nonlocal times
self.assertEqual((yield self.count), 4)
yield
self.assertEqual((yield self.count), 5)
yield
self.assertEqual((yield self.count), 6)
yield
times += 1
sim.add_sync_process(process)
sim.run()
sim.reset()
sim.run()
self.assertEqual(times, 2)
def setUp_alu(self): def setUp_alu(self):
self.a = Signal(8) self.a = Signal(8)
self.b = Signal(8) self.b = Signal(8)
@ -406,7 +437,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
def process(): def process():
yield self.i.eq(0b10101010) yield self.i.eq(0b10101010)
yield self.i[:4].eq(-1) yield self.i[:4].eq(-1)
yield Delay() yield Settle()
self.assertEqual((yield self.i[:4]), 0b1111) self.assertEqual((yield self.i[:4]), 0b1111)
self.assertEqual((yield self.i), 0b10101111) self.assertEqual((yield self.i), 0b10101111)
sim.add_process(process) sim.add_process(process)
@ -426,10 +457,18 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
def test_add_process_wrong(self): def test_add_process_wrong(self):
with self.assertSimulation(Module()) as sim: with self.assertSimulation(Module()) as sim:
with self.assertRaises(TypeError, with self.assertRaises(TypeError,
msg="Cannot add a process 1 because it is not a generator or " msg="Cannot add a process 1 because it is not a generator function"):
"a generator function"):
sim.add_process(1) sim.add_process(1)
def test_add_process_wrong_generator(self):
with self.assertSimulation(Module()) as sim:
with self.assertWarns(DeprecationWarning,
msg="instead of generators, use generator functions as processes; "
"this allows the simulator to be repeatedly reset"):
def process():
yield Delay()
sim.add_process(process())
def test_add_clock_wrong_twice(self): def test_add_clock_wrong_twice(self):
m = Module() m = Module()
s = Signal() s = Signal()
@ -452,37 +491,18 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
with self.assertSimulation(m) as sim: with self.assertSimulation(m) as sim:
sim.add_clock(1, if_exists=True) sim.add_clock(1, if_exists=True)
def test_eq_signal_unused_wrong(self):
self.setUp_lhs_rhs()
self.s = Signal()
with self.assertSimulation(self.m) as sim:
def process():
with self.assertRaisesRegex(ValueError,
regex=r"Process .+? sent a request to set signal \(sig s\), "
r"which is not a part of simulation"):
yield self.s.eq(0)
yield Delay()
sim.add_process(process)
def test_eq_signal_comb_wrong(self):
self.setUp_lhs_rhs()
with self.assertSimulation(self.m) as sim:
def process():
with self.assertRaisesRegex(ValueError,
regex=r"Process .+? sent a request to set signal \(sig o\), "
r"which is a part of combinatorial assignment in simulation"):
yield self.o.eq(0)
yield Delay()
sim.add_process(process)
def test_command_wrong(self): def test_command_wrong(self):
survived = False
with self.assertSimulation(Module()) as sim: with self.assertSimulation(Module()) as sim:
def process(): def process():
nonlocal survived
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
regex=r"Received unsupported command 1 from process .+?"): regex=r"Received unsupported command 1 from process .+?"):
yield 1 yield 1
yield Delay() yield Settle()
survived = True
sim.add_process(process) sim.add_process(process)
self.assertTrue(survived)
def setUp_memory(self, rd_synchronous=True, rd_transparent=True, wr_granularity=None): def setUp_memory(self, rd_synchronous=True, rd_transparent=True, wr_granularity=None):
self.m = Module() self.m = Module()
@ -558,7 +578,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
self.assertEqual((yield self.rdport.data), 0xaa) self.assertEqual((yield self.rdport.data), 0xaa)
yield yield
self.assertEqual((yield self.rdport.data), 0xaa) self.assertEqual((yield self.rdport.data), 0xaa)
yield Delay(1e-6) # let comb propagate yield Settle()
self.assertEqual((yield self.rdport.data), 0x33) self.assertEqual((yield self.rdport.data), 0x33)
sim.add_clock(1e-6) sim.add_clock(1e-6)
sim.add_sync_process(process) sim.add_sync_process(process)
@ -571,11 +591,11 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
yield self.wrport.en.eq(1) yield self.wrport.en.eq(1)
yield yield
self.assertEqual((yield self.rdport.data), 0xaa) self.assertEqual((yield self.rdport.data), 0xaa)
yield Delay(1e-6) # let comb propagate yield Settle()
self.assertEqual((yield self.rdport.data), 0x33) self.assertEqual((yield self.rdport.data), 0x33)
yield yield
yield self.rdport.addr.eq(1) yield self.rdport.addr.eq(1)
yield Delay(1e-6) # let comb propagate yield Settle()
self.assertEqual((yield self.rdport.data), 0x33) self.assertEqual((yield self.rdport.data), 0x33)
sim.add_clock(1e-6) sim.add_clock(1e-6)
sim.add_sync_process(process) sim.add_sync_process(process)
@ -585,10 +605,10 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
with self.assertSimulation(self.m) as sim: with self.assertSimulation(self.m) as sim:
def process(): def process():
yield self.rdport.addr.eq(0) yield self.rdport.addr.eq(0)
yield Delay() yield Settle()
self.assertEqual((yield self.rdport.data), 0xaa) self.assertEqual((yield self.rdport.data), 0xaa)
yield self.rdport.addr.eq(1) yield self.rdport.addr.eq(1)
yield Delay() yield Settle()
self.assertEqual((yield self.rdport.data), 0x55) self.assertEqual((yield self.rdport.data), 0x55)
yield self.rdport.addr.eq(0) yield self.rdport.addr.eq(0)
yield self.wrport.addr.eq(0) yield self.wrport.addr.eq(0)
@ -596,7 +616,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
yield self.wrport.en.eq(1) yield self.wrport.en.eq(1)
yield Tick("sync") yield Tick("sync")
self.assertEqual((yield self.rdport.data), 0xaa) self.assertEqual((yield self.rdport.data), 0xaa)
yield Delay(1e-6) # let comb propagate yield Settle()
self.assertEqual((yield self.rdport.data), 0x33) self.assertEqual((yield self.rdport.data), 0x33)
sim.add_clock(1e-6) sim.add_clock(1e-6)
sim.add_process(process) sim.add_process(process)
@ -661,8 +681,26 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
sim.add_sync_process(process_gen) sim.add_sync_process(process_gen)
sim.add_sync_process(process_check) sim.add_sync_process(process_check)
def test_wrong_not_run(self): def test_vcd_wrong_nonzero_time(self):
with self.assertWarns(UserWarning, s = Signal()
msg="Simulation created, but not run"): m = Module()
with Simulator(Fragment()) as sim: m.d.sync += s.eq(s)
sim = Simulator(m)
sim.add_clock(1e-6)
sim.run_until(1e-5)
with self.assertRaisesRegex(ValueError,
regex=r"^Cannot start writing waveforms after advancing simulation time$"):
with sim.write_vcd(open(os.path.devnull, "wt")):
pass
def test_vcd_wrong_twice(self):
s = Signal()
m = Module()
m.d.sync += s.eq(s)
sim = Simulator(m)
sim.add_clock(1e-6)
with self.assertRaisesRegex(ValueError,
regex=r"^Already writing waveforms to .+$"):
with sim.write_vcd(open(os.path.devnull, "wt")):
with sim.write_vcd(open(os.path.devnull, "wt")):
pass pass

View file

@ -24,7 +24,11 @@ setup(
license="BSD", license="BSD",
python_requires="~=3.6", python_requires="~=3.6",
setup_requires=["setuptools_scm"], setup_requires=["setuptools_scm"],
install_requires=["setuptools", "pyvcd>=0.1.4", "bitarray", "Jinja2"], install_requires=[
"setuptools",
"pyvcd~=0.1.4", # for nmigen.pysim
"Jinja2", # for nmigen.build
],
packages=find_packages(), packages=find_packages(),
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [