Skip to content

Commit 9c35b11

Browse files
whitequarkwanda-phi
andcommitted
Implement RFC 36.
This feature does not exactly follow the RFC because the RFC as written is not implementable; the treatment of async resets in `tick()` triggers had to be changed. In addition, iterating a trigger was made to watch for missed events, in case the body of the `async for` awaited for too long. Co-authored-by: Wanda <[email protected]>
1 parent ddcd153 commit 9c35b11

File tree

10 files changed

+1221
-351
lines changed

10 files changed

+1221
-351
lines changed

amaranth/sim/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from .core import *
22

33

4-
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
4+
__all__ = [
5+
"DomainReset", "BrokenTrigger", "Simulator",
6+
# deprecated
7+
"Settle", "Delay", "Tick", "Passive", "Active",
8+
]

amaranth/sim/_async.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import typing
2+
import operator
3+
from contextlib import contextmanager
4+
5+
from ..hdl import *
6+
from ..hdl._ast import Slice
7+
from ._base import BaseProcess, BaseEngine
8+
9+
10+
__all__ = [
11+
"DomainReset", "BrokenTrigger",
12+
"SampleTrigger", "ChangedTrigger", "EdgeTrigger", "DelayTrigger",
13+
"TriggerCombination", "TickTrigger",
14+
"SimulatorContext", "ProcessContext", "TestbenchContext", "AsyncProcess",
15+
]
16+
17+
18+
class DomainReset(Exception):
19+
"""Exception raised when the domain of a a tick trigger that is repeatedly awaited has its
20+
reset asserted."""
21+
22+
23+
class BrokenTrigger(Exception):
24+
"""Exception raised when a trigger that is repeatedly awaited in an `async for` loop has
25+
a matching event occur while the body of the `async for` loop is executing."""
26+
27+
28+
class SampleTrigger:
29+
def __init__(self, value):
30+
self.value = Value.cast(value)
31+
if isinstance(value, ValueCastable):
32+
self.shape = value.shape()
33+
else:
34+
self.shape = self.value.shape()
35+
36+
37+
class ChangedTrigger:
38+
def __init__(self, signal):
39+
cast_signal = Value.cast(signal)
40+
if not isinstance(cast_signal, Signal):
41+
raise TypeError(f"Change trigger can only be used with a signal, not {signal!r}")
42+
self.shape = signal.shape()
43+
self.signal = cast_signal
44+
45+
@property
46+
def value(self):
47+
return self.signal
48+
49+
50+
class EdgeTrigger:
51+
def __init__(self, signal, polarity):
52+
cast_signal = Value.cast(signal)
53+
if isinstance(cast_signal, Signal) and len(cast_signal) == 1:
54+
self.signal, self.bit = cast_signal, 0
55+
elif (isinstance(cast_signal, Slice) and
56+
len(cast_signal) == 1 and
57+
isinstance(cast_signal.value, Signal)):
58+
self.signal, self.bit = cast_signal.value, cast_signal.start
59+
else:
60+
raise TypeError(f"Edge trigger can only be used with a single-bit signal or "
61+
f"a single-bit slice of a signal, not {signal!r}")
62+
if polarity not in (0, 1):
63+
raise ValueError(f"Edge trigger polarity must be 0 or 1, not {polarity!r}")
64+
self.polarity = polarity
65+
66+
67+
class DelayTrigger:
68+
def __init__(self, interval):
69+
self.interval_fs = round(float(interval) * 1e15)
70+
71+
72+
class TriggerCombination:
73+
def __init__(self, engine: BaseEngine, process: BaseProcess, *,
74+
triggers: 'tuple[DelayTrigger|ChangedTrigger|SampleTrigger|EdgeTrigger, ...]' = ()):
75+
self._engine = engine
76+
self._process = process # private but used by engines
77+
self._triggers = triggers # private but used by engines
78+
79+
def sample(self, *values) -> 'TriggerCombination':
80+
return TriggerCombination(self._engine, self._process, triggers=self._triggers +
81+
tuple(SampleTrigger(value) for value in values))
82+
83+
def changed(self, *signals) -> 'TriggerCombination':
84+
return TriggerCombination(self._engine, self._process, triggers=self._triggers +
85+
tuple(ChangedTrigger(signal) for signal in signals))
86+
87+
def edge(self, signal, polarity) -> 'TriggerCombination':
88+
return TriggerCombination(self._engine, self._process, triggers=self._triggers +
89+
(EdgeTrigger(signal, polarity),))
90+
91+
def posedge(self, signal) -> 'TriggerCombination':
92+
return self.edge(signal, 1)
93+
94+
def negedge(self, signal) -> 'TriggerCombination':
95+
return self.edge(signal, 0)
96+
97+
def delay(self, interval) -> 'TriggerCombination':
98+
return TriggerCombination(self._engine, self._process, triggers=self._triggers +
99+
(DelayTrigger(interval),))
100+
101+
def __await__(self):
102+
trigger = self._engine.add_trigger_combination(self, oneshot=True)
103+
return trigger.__await__()
104+
105+
async def __aiter__(self):
106+
trigger = self._engine.add_trigger_combination(self, oneshot=False)
107+
while True:
108+
yield await trigger
109+
110+
111+
class TickTrigger:
112+
def __init__(self, engine: BaseEngine, process: BaseProcess, *,
113+
domain: ClockDomain, sampled: 'tuple[ValueLike]' = ()):
114+
self._engine = engine
115+
self._process = process
116+
self._domain = domain
117+
self._sampled = sampled
118+
119+
def sample(self, *values: ValueLike) -> 'TickTrigger':
120+
return TickTrigger(self._engine, self._process,
121+
domain=self._domain, sampled=(*self._sampled, *values))
122+
123+
async def until(self, condition: ValueLike):
124+
if not isinstance(condition, ValueLike):
125+
raise TypeError(f"Condition must be a value-like object, not {condition!r}")
126+
tick = self.sample(condition).__aiter__()
127+
done = False
128+
while not done:
129+
clk, rst, *values, done = await anext(tick)
130+
if rst:
131+
raise DomainReset
132+
return tuple(values)
133+
134+
async def repeat(self, count: int):
135+
count = operator.index(count)
136+
if count <= 0:
137+
raise ValueError(f"Repeat count must be a positive integer, not {count!r}")
138+
tick = self.__aiter__()
139+
for _ in range(count):
140+
clk, rst, *values = await anext(tick)
141+
if rst:
142+
raise DomainReset
143+
return tuple(values)
144+
145+
def _collect_trigger(self):
146+
clk_polarity = (1 if self._domain.clk_edge == "pos" else 0)
147+
if self._domain.async_reset and self._domain.rst is not None:
148+
return (TriggerCombination(self._engine, self._process)
149+
.edge(self._domain.clk, clk_polarity)
150+
.edge(self._domain.rst, 1)
151+
.sample(self._domain.rst)
152+
.sample(*self._sampled))
153+
else:
154+
return (TriggerCombination(self._engine, self._process)
155+
.edge(self._domain.clk, clk_polarity)
156+
.sample(Const(0))
157+
.sample(Const(0) if self._domain.rst is None else self._domain.rst)
158+
.sample(*self._sampled))
159+
160+
def __await__(self):
161+
trigger = self._engine.add_trigger_combination(self._collect_trigger(), oneshot=True)
162+
clk_edge, rst_edge, rst_sample, *values = yield from trigger.__await__()
163+
return (clk_edge, bool(rst_edge or rst_sample), *values)
164+
165+
async def __aiter__(self):
166+
trigger = self._engine.add_trigger_combination(self._collect_trigger(), oneshot=False)
167+
while True:
168+
clk_edge, rst_edge, rst_sample, *values = await trigger
169+
yield (clk_edge, bool(rst_edge or rst_sample), *values)
170+
171+
172+
class SimulatorContext:
173+
def __init__(self, design, engine: BaseEngine, process: BaseProcess):
174+
self._design = design
175+
self._engine = engine
176+
self._process = process
177+
178+
def delay(self, interval) -> TriggerCombination:
179+
return TriggerCombination(self._engine, self._process).delay(interval)
180+
181+
def changed(self, *signals) -> TriggerCombination:
182+
return TriggerCombination(self._engine, self._process).changed(*signals)
183+
184+
def edge(self, signal, polarity) -> TriggerCombination:
185+
return TriggerCombination(self._engine, self._process).edge(signal, polarity)
186+
187+
def posedge(self, signal) -> TriggerCombination:
188+
return TriggerCombination(self._engine, self._process).posedge(signal)
189+
190+
def negedge(self, signal) -> TriggerCombination:
191+
return TriggerCombination(self._engine, self._process).negedge(signal)
192+
193+
@typing.overload
194+
def tick(self, domain: str, *, context: Elaboratable = None) -> TickTrigger: ... # :nocov:
195+
196+
@typing.overload
197+
def tick(self, domain: ClockDomain) -> TickTrigger: ... # :nocov:
198+
199+
def tick(self, domain="sync", *, context=None):
200+
if domain == "comb":
201+
raise ValueError("Combinational domain does not have a clock")
202+
if isinstance(domain, ClockDomain):
203+
if context is not None:
204+
raise ValueError("Context cannot be provided if a clock domain is specified "
205+
"directly")
206+
else:
207+
domain = self._design.lookup_domain(domain, context)
208+
return TickTrigger(self._engine, self._process, domain=domain)
209+
210+
@contextmanager
211+
def critical(self):
212+
try:
213+
old_critical, self._process.critical = self._process.critical, True
214+
yield
215+
finally:
216+
self._process.critical = old_critical
217+
218+
219+
class ProcessContext(SimulatorContext):
220+
def get(self, expr: ValueLike) -> 'typing.Never':
221+
raise TypeError("`.get()` cannot be used to sample values in simulator processes; use "
222+
"`.sample()` on a trigger object instead")
223+
224+
@typing.overload
225+
def set(self, expr: Value, value: int) -> None: ... # :nocov:
226+
227+
@typing.overload
228+
def set(self, expr: ValueCastable, value: typing.Any) -> None: ... # :nocov:
229+
230+
def set(self, expr, value):
231+
if isinstance(expr, ValueCastable):
232+
shape = expr.shape()
233+
if isinstance(shape, ShapeCastable):
234+
value = shape.const(value)
235+
value = Const.cast(value).value
236+
self._engine.set_value(expr, value)
237+
238+
239+
class TestbenchContext(SimulatorContext):
240+
@typing.overload
241+
def get(self, expr: Value) -> int: ... # :nocov:
242+
243+
@typing.overload
244+
def get(self, expr: ValueCastable) -> typing.Any: ... # :nocov:
245+
246+
def get(self, expr):
247+
value = self._engine.get_value(expr)
248+
if isinstance(expr, ValueCastable):
249+
shape = expr.shape()
250+
if isinstance(shape, ShapeCastable):
251+
return shape.from_bits(value)
252+
return value
253+
254+
@typing.overload
255+
def set(self, expr: Value, value: int) -> None: ... # :nocov:
256+
257+
@typing.overload
258+
def set(self, expr: ValueCastable, value: typing.Any) -> None: ... # :nocov:
259+
260+
def set(self, expr, value):
261+
if isinstance(expr, ValueCastable):
262+
shape = expr.shape()
263+
if isinstance(shape, ShapeCastable):
264+
value = shape.const(value)
265+
value = Const.cast(value).value
266+
self._engine.set_value(expr, value)
267+
self._engine.step_design()
268+
269+
270+
class AsyncProcess(BaseProcess):
271+
def __init__(self, design, engine, constructor, *, testbench, background):
272+
self.constructor = constructor
273+
if testbench:
274+
self.context = TestbenchContext(design, engine, self)
275+
else:
276+
self.context = ProcessContext(design, engine, self)
277+
self.background = background
278+
279+
self.reset()
280+
281+
def reset(self):
282+
self.runnable = True
283+
self.critical = not self.background
284+
self.waits_on = None
285+
self.coroutine = self.constructor(self.context)
286+
287+
def run(self):
288+
try:
289+
self.waits_on = self.coroutine.send(None)
290+
except StopIteration:
291+
self.critical = False
292+
self.waits_on = None
293+
self.coroutine = None

0 commit comments

Comments
 (0)