sim: represent time internally as 1ps units
authormodwizcode <irides@irides.network>
Mon, 13 Dec 2021 03:43:20 +0000 (21:43 -0600)
committerLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Fri, 31 Dec 2021 20:29:07 +0000 (20:29 +0000)
Using floats to represent simulation time internally isn't ideal
instead use 1ps internal units while continuing to use a floating
point based interface for compatibility.

Fixes #535.

nmigen/sim/_pyclock.py
nmigen/sim/_pycoro.py
nmigen/sim/core.py
nmigen/sim/pysim.py
tests/test_sim.py

index ca74fb3ee696d78f6cf4a9e9280e527c7dc44905..816ef903c3ed999f674613e4b2ce20462913cd5b 100644 (file)
@@ -31,4 +31,4 @@ class PyClockProcess(BaseProcess):
         else:
             clk_state = self.state.slots[self.slot]
             clk_state.set(not clk_state.curr)
-            self.state.wait_interval(self, self.period / 2)
+            self.state.wait_interval(self, self.period // 2)
index 554187c50ce09ae60246a82ef84ae6ee238e89fa..6e8d6969700b66464d7dfa442052fabcc976d980 100644 (file)
@@ -95,7 +95,9 @@ class PyCoroProcess(BaseProcess):
                     return
 
                 elif type(command) is Delay:
-                    self.state.wait_interval(self, command.interval)
+                    # Internal timeline is in 1ps integeral units, intervals are public API and in floating point
+                    interval = int(command.interval * 1e12) if command.interval is not None else None
+                    self.state.wait_interval(self, interval)
                     return
 
                 elif type(command) is Passive:
index 270f8475beae4af25714fa2b4f49b6d4ee2c3f93..64a92be62397237ed21588b50c307ee855725aa3 100644 (file)
@@ -130,12 +130,15 @@ class Simulator:
         if domain in self._clocked:
             raise ValueError("Domain {!r} already has a clock driving it"
                              .format(domain.name))
+        
+        # We represent times internally in 1 ps units, but users supply float quantities of seconds
+        period = int(period * 1e12)
 
         if phase is None:
             # By default, delay the first edge by half period. This causes any synchronous activity
             # to happen at a non-zero time, distinguishing it from the reset values in the waveform
             # viewer.
-            phase = period / 2
+            phase = period // 2
         self._engine.add_clock_process(domain.clk, phase=phase, period=period)
         self._clocked.add(domain)
 
@@ -181,6 +184,8 @@ class Simulator:
 
         If the simulation stops advancing, this function will never return.
         """
+        # Convert deadline in seconds into internal 1 ps units
+        deadline = deadline * 1e12
         assert self._engine.now <= deadline
         while (self.advance() or run_passive) and self._engine.now < deadline:
             pass
@@ -204,7 +209,7 @@ class Simulator:
         traces : iterable of Signal
             Signals to display traces for.
         """
-        if self._engine.now != 0.0:
+        if self._engine.now != 0:
             for file in (vcd_file, gtkw_file):
                 if hasattr(file, "close"):
                     file.close()
index 0d8b65908413acf9d659b0ff6e308c5dd8fecfaf..cc3d9483854ed0722201156335a43e16f2757df3 100644 (file)
@@ -48,10 +48,6 @@ class _NameExtractor:
 
 
 class _VCDWriter:
-    @staticmethod
-    def timestamp_to_vcd(timestamp):
-        return timestamp * (10 ** 10) # 1/(100 ps)
-
     @staticmethod
     def decode_to_vcd(signal, value):
         return signal.decoder(value).expandtabs().replace(" ", "_")
@@ -65,7 +61,7 @@ class _VCDWriter:
         self.vcd_vars = SignalDict()
         self.vcd_file = vcd_file
         self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
-            timescale="100 ps", comment="Generated by nMigen")
+            timescale="1 ps", comment="Generated by nMigen")
 
         self.gtkw_names = SignalDict()
         self.gtkw_file = gtkw_file
@@ -127,16 +123,15 @@ class _VCDWriter:
         if vcd_var is None:
             return
 
-        vcd_timestamp = self.timestamp_to_vcd(timestamp)
         if signal.decoder:
             var_value = self.decode_to_vcd(signal, value)
         else:
             var_value = value
-        self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)
+        self.vcd_writer.change(vcd_var, timestamp, var_value)
 
     def close(self, timestamp):
         if self.vcd_writer is not None:
-            self.vcd_writer.close(self.timestamp_to_vcd(timestamp))
+            self.vcd_writer.close(timestamp)
 
         if self.gtkw_save is not None:
             self.gtkw_save.dumpfile(self.vcd_file.name)
@@ -158,11 +153,11 @@ class _VCDWriter:
 
 class _Timeline:
     def __init__(self):
-        self.now = 0.0
+        self.now = 0
         self.deadlines = dict()
 
     def reset(self):
-        self.now = 0.0
+        self.now = 0
         self.deadlines.clear()
 
     def at(self, run_at, process):
index 8bf100a61f72f68f8589f4937599ae1544f4b971..94b1141a2951a2253e520c2ac3aa0000ba4fddc6 100644 (file)
@@ -571,6 +571,19 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 self.fail()
             sim.add_process(process)
 
+    def test_run_until_fail(self):
+        m = Module()
+        s = Signal()
+        m.d.sync += s.eq(0)
+        with self.assertRaises(AssertionError):
+            with self.assertSimulation(m, deadline=100e-6) as sim:
+                    sim.add_clock(1e-6)
+                    def process():
+                        for _ in range(99):
+                            yield Delay(1e-6)
+                        self.fail()
+                    sim.add_process(process)
+
     def test_add_process_wrong(self):
         with self.assertSimulation(Module()) as sim:
             with self.assertRaisesRegex(TypeError,