import os import tempfile import warnings import inspect from contextlib import contextmanager import itertools from vcd import VCDWriter from vcd.gtkw import GTKWSave from .._utils import deprecated from ..hdl.ast import * from ..hdl.cd import * from ..hdl.ir import * from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter class Command: pass class Settle(Command): def __repr__(self): return "(settle)" class Delay(Command): def __init__(self, interval=None): self.interval = None if interval is None else float(interval) def __repr__(self): if self.interval is None: return "(delay ε)" else: return "(delay {:.3}us)".format(self.interval * 1e6) class Tick(Command): def __init__(self, domain="sync"): if not isinstance(domain, (str, ClockDomain)): raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}" .format(domain)) assert domain != "comb" self.domain = domain def __repr__(self): return "(tick {})".format(self.domain) class Passive(Command): def __repr__(self): return "(passive)" class Active(Command): def __repr__(self): return "(active)" class _WaveformWriter: def update(self, timestamp, signal, value): raise NotImplementedError # :nocov: def close(self, timestamp): raise NotImplementedError # :nocov: class _VCDWaveformWriter(_WaveformWriter): @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(" ", "_") def __init__(self, signal_names, *, vcd_file, gtkw_file=None, traces=()): if isinstance(vcd_file, str): vcd_file = open(vcd_file, "wt") if isinstance(gtkw_file, str): gtkw_file = open(gtkw_file, "wt") 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") self.gtkw_names = SignalDict() self.gtkw_file = gtkw_file self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file) self.traces = [] trace_names = SignalDict() for trace in traces: if trace not in signal_names: trace_names[trace] = trace.name self.traces.append(trace) if self.vcd_writer is None: return for signal, names in itertools.chain(signal_names.items(), trace_names.items()): if signal.decoder: var_type = "string" var_size = 1 var_init = self.decode_to_vcd(signal, signal.reset) else: var_type = "wire" var_size = signal.width var_init = signal.reset for (*var_scope, var_name) in names: suffix = None while True: try: if suffix is None: var_name_suffix = var_name else: var_name_suffix = "{}${}".format(var_name, suffix) vcd_var = self.vcd_writer.register_var( scope=var_scope, name=var_name_suffix, var_type=var_type, size=var_size, init=var_init) break except KeyError: suffix = (suffix or 0) + 1 if signal not in self.vcd_vars: self.vcd_vars[signal] = set() self.vcd_vars[signal].add(vcd_var) if signal not in self.gtkw_names: self.gtkw_names[signal] = (*var_scope, var_name_suffix) def update(self, timestamp, signal, value): if signal not in self.vcd_vars: return vcd_timestamp = self.timestamp_to_vcd(timestamp) if signal.decoder: var_value = self.decode_to_vcd(signal, value) else: var_value = value for vcd_var in self.vcd_vars[signal]: self.vcd_writer.change(vcd_var, vcd_timestamp, var_value) def close(self, timestamp): if self.vcd_writer is not None: self.vcd_writer.close(self.timestamp_to_vcd(timestamp)) if self.gtkw_save is not None: self.gtkw_save.dumpfile(self.vcd_file.name) self.gtkw_save.dumpfile_size(self.vcd_file.tell()) self.gtkw_save.treeopen("top") for signal in self.traces: if len(signal) > 1 and not signal.decoder: suffix = "[{}:0]".format(len(signal) - 1) else: suffix = "" self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix) if self.vcd_file is not None: self.vcd_file.close() if self.gtkw_file is not None: self.gtkw_file.close() class _Process: __slots__ = ("runnable", "passive") def reset(self): raise NotImplementedError # :nocov: def run(self): raise NotImplementedError # :nocov: @property def name(self): raise NotImplementedError # :nocov: class _SignalState: __slots__ = ("signal", "curr", "next", "waiters", "pending") def __init__(self, signal, pending): self.signal = signal self.pending = pending self.waiters = dict() self.reset() def reset(self): self.curr = self.next = self.signal.reset def set(self, value): if self.next == value: return self.next = value self.pending.add(self) def wait(self, task, *, trigger=None): assert task not in self.waiters self.waiters[task] = trigger def commit(self): if self.curr == self.next: return False self.curr = self.next return True def wakeup(self): awoken_any = False for process, trigger in self.waiters.items(): if trigger is None or trigger == self.curr: process.runnable = awoken_any = True return awoken_any class _SimulatorState: def __init__(self): self.signals = SignalDict() self.pending = set() self.timestamp = 0.0 self.deadlines = dict() self.waveform_writer = None def reset(self): for signal_state in self.signals.values(): signal_state.reset() self.pending.clear() self.timestamp = 0.0 self.deadlines.clear() def for_signal(self, signal): try: return self.signals[signal] except KeyError: signal_state = _SignalState(signal, self.pending) self.signals[signal] = signal_state return signal_state def commit(self): awoken_any = False for signal_state in self.pending: if signal_state.commit(): if signal_state.wakeup(): awoken_any = True if self.waveform_writer is not None: self.waveform_writer.update(self.timestamp, signal_state.signal, signal_state.curr) return awoken_any def advance(self): nearest_processes = set() nearest_deadline = None for process, deadline in self.deadlines.items(): if deadline is None: if nearest_deadline is not None: nearest_processes.clear() nearest_processes.add(process) nearest_deadline = self.timestamp break elif nearest_deadline is None or deadline <= nearest_deadline: assert deadline >= self.timestamp if nearest_deadline is not None and deadline < nearest_deadline: nearest_processes.clear() nearest_processes.add(process) nearest_deadline = deadline if not nearest_processes: return False for process in nearest_processes: process.runnable = True del self.deadlines[process] self.timestamp = nearest_deadline return True def start_waveform(self, waveform_writer): if self.timestamp != 0.0: raise ValueError("Cannot start writing waveforms after advancing simulation time") if self.waveform_writer is not None: raise ValueError("Already writing waveforms to {!r}" .format(self.waveform_writer)) self.waveform_writer = waveform_writer def finish_waveform(self): if self.waveform_writer is None: return self.waveform_writer.close(self.timestamp) self.waveform_writer = None class _EvalContext: __slots__ = ("state", "indexes", "slots") def __init__(self, state): self.state = state self.indexes = SignalDict() self.slots = [] def get_signal(self, signal): try: return self.indexes[signal] except KeyError: index = len(self.slots) self.slots.append(self.state.for_signal(signal)) self.indexes[signal] = index return index def get_in_signal(self, signal, *, trigger=None): index = self.get_signal(signal) self.slots[index].waiters[self] = trigger return index def get_out_signal(self, signal): return self.get_signal(signal) class _Emitter: def __init__(self): self._buffer = [] self._suffix = 0 self._level = 0 def append(self, code): self._buffer.append(" " * self._level) self._buffer.append(code) self._buffer.append("\n") @contextmanager def indent(self): self._level += 1 yield self._level -= 1 def flush(self, indent=""): code = "".join(self._buffer) self._buffer.clear() return code def gen_var(self, prefix): name = f"{prefix}_{self._suffix}" self._suffix += 1 return name def def_var(self, prefix, value): name = self.gen_var(prefix) self.append(f"{name} = {value}") return name class _Compiler: def __init__(self, context, emitter): self.context = context self.emitter = emitter class _ValueCompiler(ValueVisitor, _Compiler): helpers = { "sign": lambda value, sign: value | sign if value & sign else value, "zdiv": lambda lhs, rhs: 0 if rhs == 0 else lhs // rhs, } def on_ClockSignal(self, value): raise NotImplementedError # :nocov: def on_ResetSignal(self, value): raise NotImplementedError # :nocov: def on_Record(self, value): return self(Cat(value.fields.values())) def on_AnyConst(self, value): raise NotImplementedError # :nocov: def on_AnySeq(self, value): raise NotImplementedError # :nocov: def on_Sample(self, value): raise NotImplementedError # :nocov: def on_Initial(self, value): raise NotImplementedError # :nocov: class _RHSValueCompiler(_ValueCompiler): def __init__(self, context, emitter, *, mode, inputs=None): super().__init__(context, emitter) assert mode in ("curr", "next") self.mode = mode # If not None, `inputs` gets populated with RHS signals. self.inputs = inputs def on_Const(self, value): return f"{value.value}" def on_Signal(self, value): if self.inputs is not None: self.inputs.add(value) if self.mode == "curr": return f"slots[{self.context.get_signal(value)}].{self.mode}" else: return f"next_{self.context.get_signal(value)}" def on_Operator(self, value): def mask(value): value_mask = (1 << len(value)) - 1 return f"({self(value)} & {value_mask})" def sign(value): if value.shape().signed: return f"sign({mask(value)}, {-1 << (len(value) - 1)})" else: # unsigned return mask(value) if len(value.operands) == 1: arg, = value.operands if value.operator == "~": return f"(~{self(arg)})" if value.operator == "-": return f"(-{self(arg)})" if value.operator == "b": return f"bool({mask(arg)})" if value.operator == "r|": return f"({mask(arg)} != 0)" if value.operator == "r&": return f"({mask(arg)} == {(1 << len(arg)) - 1})" if value.operator == "r^": # Believe it or not, this is the fastest way to compute a sideways XOR in Python. return f"(format({mask(arg)}, 'b').count('1') % 2)" if value.operator in ("u", "s"): # These operators don't change the bit pattern, only its interpretation. return self(arg) elif len(value.operands) == 2: lhs, rhs = value.operands lhs_mask = (1 << len(lhs)) - 1 rhs_mask = (1 << len(rhs)) - 1 if value.operator == "+": return f"({sign(lhs)} + {sign(rhs)})" if value.operator == "-": return f"({sign(lhs)} - {sign(rhs)})" if value.operator == "*": return f"({sign(lhs)} * {sign(rhs)})" if value.operator == "//": return f"zdiv({sign(lhs)}, {sign(rhs)})" if value.operator == "&": return f"({self(lhs)} & {self(rhs)})" if value.operator == "|": return f"({self(lhs)} | {self(rhs)})" if value.operator == "^": return f"({self(lhs)} ^ {self(rhs)})" if value.operator == "<<": return f"({sign(lhs)} << {sign(rhs)})" if value.operator == ">>": return f"({sign(lhs)} >> {sign(rhs)})" if value.operator == "==": return f"({sign(lhs)} == {sign(rhs)})" if value.operator == "!=": return f"({sign(lhs)} != {sign(rhs)})" if value.operator == "<": return f"({sign(lhs)} < {sign(rhs)})" if value.operator == "<=": return f"({sign(lhs)} <= {sign(rhs)})" if value.operator == ">": return f"({sign(lhs)} > {sign(rhs)})" if value.operator == ">=": return f"({sign(lhs)} >= {sign(rhs)})" elif len(value.operands) == 3: if value.operator == "m": sel, val1, val0 = value.operands return f"({self(val1)} if {self(sel)} else {self(val0)})" raise NotImplementedError("Operator '{}' not implemented".format(value.operator)) # :nocov: def on_Slice(self, value): return f"(({self(value.value)} >> {value.start}) & {(1 << len(value)) - 1})" def on_Part(self, value): offset_mask = (1 << len(value.offset)) - 1 offset = f"(({self(value.offset)} & {offset_mask}) * {value.stride})" return f"({self(value.value)} >> {offset} & " \ f"{(1 << value.width) - 1})" def on_Cat(self, value): gen_parts = [] offset = 0 for part in value.parts: part_mask = (1 << len(part)) - 1 gen_parts.append(f"(({self(part)} & {part_mask}) << {offset})") offset += len(part) if gen_parts: return f"({' | '.join(gen_parts)})" return f"0" def on_Repl(self, value): part_mask = (1 << len(value.value)) - 1 gen_part = self.emitter.def_var("repl", f"{self(value.value)} & {part_mask}") gen_parts = [] offset = 0 for _ in range(value.count): gen_parts.append(f"({gen_part} << {offset})") offset += len(value.value) if gen_parts: return f"({' | '.join(gen_parts)})" return f"0" def on_ArrayProxy(self, value): index_mask = (1 << len(value.index)) - 1 gen_index = self.emitter.def_var("rhs_index", f"{self(value.index)} & {index_mask}") gen_value = self.emitter.gen_var("rhs_proxy") if value.elems: gen_elems = [] for index, elem in enumerate(value.elems): if index == 0: self.emitter.append(f"if {gen_index} == {index}:") else: self.emitter.append(f"elif {gen_index} == {index}:") with self.emitter.indent(): self.emitter.append(f"{gen_value} = {self(elem)}") self.emitter.append(f"else:") with self.emitter.indent(): self.emitter.append(f"{gen_value} = {self(value.elems[-1])}") return gen_value else: return f"0" @classmethod def compile(cls, context, value, *, mode, inputs=None): emitter = _Emitter() compiler = cls(context, emitter, mode=mode, inputs=inputs) emitter.append(f"result = {compiler(value)}") return emitter.flush() class _LHSValueCompiler(_ValueCompiler): def __init__(self, context, emitter, *, rhs, outputs=None): super().__init__(context, emitter) # `rrhs` is used to translate rvalues that are syntactically a part of an lvalue, e.g. # the offset of a Part. self.rrhs = rhs # `lrhs` is used to translate the read part of a read-modify-write cycle during partial # update of an lvalue. self.lrhs = _RHSValueCompiler(context, emitter, mode="next", inputs=None) # If not None, `outputs` gets populated with signals on LHS. self.outputs = outputs def on_Const(self, value): raise TypeError # :nocov: def on_Signal(self, value): if self.outputs is not None: self.outputs.add(value) def gen(arg): value_mask = (1 << len(value)) - 1 if value.shape().signed: value_sign = f"sign({arg} & {value_mask}, {-1 << (len(value) - 1)})" else: # unsigned value_sign = f"{arg} & {value_mask}" self.emitter.append(f"next_{self.context.get_out_signal(value)} = {value_sign}") return gen def on_Operator(self, value): raise TypeError # :nocov: def on_Slice(self, value): def gen(arg): width_mask = (1 << (value.stop - value.start)) - 1 self(value.value)(f"({self.lrhs(value.value)} & " \ f"{~(width_mask << value.start)} | " \ f"(({arg} & {width_mask}) << {value.start}))") return gen def on_Part(self, value): def gen(arg): width_mask = (1 << value.width) - 1 offset_mask = (1 << len(value.offset)) - 1 offset = f"(({self.rrhs(value.offset)} & {offset_mask}) * {value.stride})" self(value.value)(f"({self.lrhs(value.value)} & " \ f"~({width_mask} << {offset}) | " \ f"(({arg} & {width_mask}) << {offset}))") return gen def on_Cat(self, value): def gen(arg): gen_arg = self.emitter.def_var("cat", arg) gen_parts = [] offset = 0 for part in value.parts: part_mask = (1 << len(part)) - 1 self(part)(f"(({gen_arg} >> {offset}) & {part_mask})") offset += len(part) return gen def on_Repl(self, value): raise TypeError # :nocov: def on_ArrayProxy(self, value): def gen(arg): index_mask = (1 << len(value.index)) - 1 gen_index = self.emitter.def_var("index", f"{self.rrhs(value.index)} & {index_mask}") if value.elems: gen_elems = [] for index, elem in enumerate(value.elems): if index == 0: self.emitter.append(f"if {gen_index} == {index}:") else: self.emitter.append(f"elif {gen_index} == {index}:") with self.emitter.indent(): self(elem)(arg) self.emitter.append(f"else:") with self.emitter.indent(): self(value.elems[-1])(arg) else: self.emitter.append(f"pass") return gen @classmethod def compile(cls, context, stmt, *, inputs=None, outputs=None): emitter = _Emitter() compiler = cls(context, emitter, inputs=inputs, outputs=outputs) compiler(stmt) return emitter.flush() class _StatementCompiler(StatementVisitor, _Compiler): def __init__(self, context, emitter, *, inputs=None, outputs=None): super().__init__(context, emitter) self.rhs = _RHSValueCompiler(context, emitter, mode="curr", inputs=inputs) self.lhs = _LHSValueCompiler(context, emitter, rhs=self.rhs, outputs=outputs) def on_statements(self, stmts): for stmt in stmts: self(stmt) if not stmts: self.emitter.append("pass") def on_Assign(self, stmt): return self.lhs(stmt.lhs)(self.rhs(stmt.rhs)) def on_Switch(self, stmt): gen_test = self.emitter.def_var("test", f"{self.rhs(stmt.test)} & {(1 << len(stmt.test)) - 1}") for index, (patterns, stmts) in enumerate(stmt.cases.items()): gen_checks = [] if not patterns: gen_checks.append(f"True") else: for pattern in patterns: if "-" in pattern: mask = int("".join("0" if b == "-" else "1" for b in pattern), 2) value = int("".join("0" if b == "-" else b for b in pattern), 2) gen_checks.append(f"({gen_test} & {mask}) == {value}") else: value = int(pattern, 2) gen_checks.append(f"{gen_test} == {value}") if index == 0: self.emitter.append(f"if {' or '.join(gen_checks)}:") else: self.emitter.append(f"elif {' or '.join(gen_checks)}:") with self.emitter.indent(): self(stmts) def on_Assert(self, stmt): raise NotImplementedError # :nocov: def on_Assume(self, stmt): raise NotImplementedError # :nocov: def on_Cover(self, stmt): raise NotImplementedError # :nocov: @classmethod def compile(cls, context, stmt, *, inputs=None, outputs=None): output_indexes = [context.get_signal(signal) for signal in stmt._lhs_signals()] emitter = _Emitter() for signal_index in output_indexes: emitter.append(f"next_{signal_index} = slots[{signal_index}].next") compiler = cls(context, emitter, inputs=inputs, outputs=outputs) compiler(stmt) for signal_index in output_indexes: emitter.append(f"slots[{signal_index}].set(next_{signal_index})") return emitter.flush() class _CompiledProcess(_Process): __slots__ = ("context", "comb", "name", "run") def __init__(self, state, *, comb, name): self.context = _EvalContext(state) self.comb = comb self.name = name self.run = None # set by _FragmentCompiler self.reset() def reset(self): self.runnable = self.comb self.passive = True class _FragmentCompiler: def __init__(self, state, signal_names): self.state = state self.signal_names = signal_names def __call__(self, fragment, *, hierarchy=("top",)): processes = set() def add_signal_name(signal): hierarchical_signal_name = (*hierarchy, signal.name) if signal not in self.signal_names: self.signal_names[signal] = {hierarchical_signal_name} else: self.signal_names[signal].add(hierarchical_signal_name) for domain_name, domain_signals in fragment.drivers.items(): domain_stmts = LHSGroupFilter(domain_signals)(fragment.statements) domain_process = _CompiledProcess(self.state, comb=domain_name is None, name=".".join((*hierarchy, "<{}>".format(domain_name or "comb")))) emitter = _Emitter() emitter.append(f"def run():") emitter._level += 1 if domain_name is None: for signal in domain_signals: signal_index = domain_process.context.get_signal(signal) emitter.append(f"next_{signal_index} = {signal.reset}") inputs = SignalSet() _StatementCompiler(domain_process.context, emitter, inputs=inputs)(domain_stmts) for input in inputs: self.state.for_signal(input).wait(domain_process) else: domain = fragment.domains[domain_name] add_signal_name(domain.clk) if domain.rst is not None: add_signal_name(domain.rst) clk_trigger = 1 if domain.clk_edge == "pos" else 0 self.state.for_signal(domain.clk).wait(domain_process, trigger=clk_trigger) if domain.rst is not None and domain.async_reset: rst_trigger = 1 self.state.for_signal(domain.rst).wait(domain_process, trigger=rst_trigger) gen_asserts = [] clk_index = domain_process.context.get_signal(domain.clk) gen_asserts.append(f"slots[{clk_index}].curr == {clk_trigger}") if domain.rst is not None and domain.async_reset: rst_index = domain_process.context.get_signal(domain.rst) gen_asserts.append(f"slots[{rst_index}].curr == {rst_trigger}") emitter.append(f"assert {' or '.join(gen_asserts)}") for signal in domain_signals: signal_index = domain_process.context.get_signal(signal) emitter.append(f"next_{signal_index} = slots[{signal_index}].next") _StatementCompiler(domain_process.context, emitter)(domain_stmts) for signal in domain_signals: signal_index = domain_process.context.get_signal(signal) emitter.append(f"slots[{signal_index}].set(next_{signal_index})") # There shouldn't be any exceptions raised by the generated code, but if there are # (almost certainly due to a bug in the code generator), use this environment variable # to make backtraces useful. code = emitter.flush() if os.getenv("NMIGEN_pysim_dump"): file = tempfile.NamedTemporaryFile("w", prefix="nmigen_pysim_", delete=False) file.write(code) filename = file.name else: filename = "" exec_locals = {"slots": domain_process.context.slots, **_ValueCompiler.helpers} exec(compile(code, filename, "exec"), exec_locals) domain_process.run = exec_locals["run"] processes.add(domain_process) for used_signal in domain_process.context.indexes: add_signal_name(used_signal) for subfragment_index, (subfragment, subfragment_name) in enumerate(fragment.subfragments): if subfragment_name is None: subfragment_name = "U${}".format(subfragment_index) processes.update(self(subfragment, hierarchy=(*hierarchy, subfragment_name))) return processes class _CoroutineProcess(_Process): def __init__(self, state, domains, constructor, *, default_cmd=None): self.state = state self.domains = domains self.constructor = constructor self.default_cmd = default_cmd self.reset() def reset(self): self.runnable = True self.passive = False self.coroutine = self.constructor() self.eval_context = _EvalContext(self.state) self.exec_locals = { "slots": self.eval_context.slots, "result": None, **_ValueCompiler.helpers } self.waits_on = set() @property def name(self): coroutine = self.coroutine while coroutine.gi_yieldfrom is not None: coroutine = coroutine.gi_yieldfrom if inspect.isgenerator(coroutine): frame = coroutine.gi_frame if inspect.iscoroutine(coroutine): frame = coroutine.cr_frame return "{}:{}".format(inspect.getfile(frame), inspect.getlineno(frame)) def get_in_signal(self, signal, *, trigger=None): signal_state = self.state.for_signal(signal) assert self not in signal_state.waiters signal_state.waiters[self] = trigger self.waits_on.add(signal_state) return signal_state def run(self): if self.coroutine is None: return if self.waits_on: for signal_state in self.waits_on: del signal_state.waiters[self] self.waits_on.clear() response = None while True: try: command = self.coroutine.send(response) if command is None: command = self.default_cmd response = None if isinstance(command, Value): exec(_RHSValueCompiler.compile(self.eval_context, command, mode="curr"), self.exec_locals) response = Const.normalize(self.exec_locals["result"], command.shape()) elif isinstance(command, Statement): exec(_StatementCompiler.compile(self.eval_context, command), self.exec_locals) elif type(command) is Tick: domain = command.domain if isinstance(domain, ClockDomain): pass elif domain in self.domains: domain = self.domains[domain] else: raise NameError("Received command {!r} that refers to a nonexistent " "domain {!r} from process {!r}" .format(command, command.domain, self.name)) self.get_in_signal(domain.clk, trigger=1 if domain.clk_edge == "pos" else 0) if domain.rst is not None and domain.async_reset: self.get_in_signal(domain.rst, trigger=1) return elif type(command) is Settle: self.state.deadlines[self] = None return elif type(command) is Delay: if command.interval is None: self.state.deadlines[self] = None else: self.state.deadlines[self] = self.state.timestamp + command.interval return elif type(command) is Passive: self.passive = True elif type(command) is Active: self.passive = False elif command is None: # only possible if self.default_cmd is None raise TypeError("Received default command from process {!r} that was added " "with add_process(); did you mean to add this process with " "add_sync_process() instead?" .format(self.name)) else: raise TypeError("Received unsupported command {!r} from process {!r}" .format(command, self.name)) except StopIteration: self.passive = True self.coroutine = None return except Exception as exn: self.coroutine.throw(exn) class _WaveformContextManager: def __init__(self, state, waveform_writer): self._state = state self._waveform_writer = waveform_writer def __enter__(self): try: self._state.start_waveform(self._waveform_writer) except: self._waveform_writer.close(0) raise def __exit__(self, *args): self._state.finish_waveform() class Simulator: def __init__(self, fragment, **kwargs): self._state = _SimulatorState() self._signal_names = SignalDict() self._fragment = Fragment.get(fragment, platform=None).prepare() self._processes = _FragmentCompiler(self._state, self._signal_names)(self._fragment) if kwargs: # :nocov: # TODO(nmigen-0.3): remove self._state.start_waveform(_VCDWaveformWriter(self._signal_names, **kwargs)) self._clocked = set() def _check_process(self, process): if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)): if inspect.isgenerator(process) or inspect.iscoroutine(process): warnings.warn("instead of generators, use generator functions as processes; " "this allows the simulator to be repeatedly reset", DeprecationWarning, stacklevel=3) def wrapper(): yield from process return wrapper else: raise TypeError("Cannot add a process {!r} because it is not a generator function" .format(process)) return process def _add_coroutine_process(self, process, *, default_cmd): self._processes.add(_CoroutineProcess(self._state, self._fragment.domains, process, default_cmd=default_cmd)) def add_process(self, process): process = self._check_process(process) def wrapper(): # Only start a bench process after comb settling, so that the reset values are correct. yield Settle() yield from process() self._add_coroutine_process(wrapper, default_cmd=None) def add_sync_process(self, process, *, domain="sync"): process = self._check_process(process) def wrapper(): # Only start a sync process after the first clock edge (or reset edge, if the domain # uses an asynchronous reset). This matches the behavior of synchronous FFs. yield Tick(domain) yield from process() return self._add_coroutine_process(wrapper, default_cmd=Tick(domain)) def add_clock(self, period, *, phase=None, domain="sync", if_exists=False): """Add a clock process. Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle. Arguments --------- period : float Clock period. The process will toggle the ``domain`` clock signal every ``period / 2`` seconds. phase : None or float Clock phase. The process will wait ``phase`` seconds before the first clock transition. If not specified, defaults to ``period / 2``. domain : str or ClockDomain Driven clock domain. If specified as a string, the domain with that name is looked up in the root fragment of the simulation. if_exists : bool If ``False`` (the default), raise an error if the driven domain is specified as a string and the root fragment does not have such a domain. If ``True``, do nothing in this case. """ if isinstance(domain, ClockDomain): pass elif domain in self._fragment.domains: domain = self._fragment.domains[domain] elif if_exists: return else: raise ValueError("Domain {!r} is not present in simulation" .format(domain)) if domain in self._clocked: raise ValueError("Domain {!r} already has a clock driving it" .format(domain.name)) half_period = period / 2 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 = half_period def clk_process(): yield Passive() yield Delay(phase) # Behave correctly if the process is added after the clock signal is manipulated, or if # its reset state is high. initial = (yield domain.clk) while True: yield domain.clk.eq(~initial) yield Delay(half_period) yield domain.clk.eq(initial) yield Delay(half_period) self._add_coroutine_process(clk_process, default_cmd=None) self._clocked.add(domain) def reset(self): """Reset the simulation. Assign the reset value to every signal in the simulation, and restart every user process. """ self._state.reset() for process in self._processes: process.reset() def _delta(self): """Perform a delta cycle. Performs the two phases of a delta cycle: 1. run and suspend every non-waiting process once, queueing signal changes; 2. commit every queued signal change, waking up any waiting process. """ for process in self._processes: if process.runnable: process.runnable = False process.run() return self._state.commit() def _settle(self): """Settle the simulation. Run every process and commit changes until a fixed point is reached. If there is an unstable combinatorial loop, this function will never return. """ while self._delta(): pass def step(self): """Step the simulation. Run every process and commit changes until a fixed point is reached, then advance time to the closest deadline (if any). If there is an unstable combinatorial loop, this function will never return. Returns ``True`` if there are any active processes, ``False`` otherwise. """ self._settle() self._state.advance() return any(not process.passive for process in self._processes) def run(self): """Run the simulation while any processes are active. Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active, and may change their status using the ``yield Passive()`` and ``yield Active()`` commands. Processes compiled from HDL and added with :meth:`add_clock` are always passive. """ while self.step(): pass def run_until(self, deadline, *, run_passive=False): """Run the simulation until it advances to ``deadline``. If ``run_passive`` is ``False``, the simulation also stops when there are no active processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it advances to or past ``deadline``. If the simulation stops advancing, this function will never return. """ assert self._state.timestamp <= deadline while (self.step() or run_passive) and self._state.timestamp < deadline: pass def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()): """Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file. This method returns a context manager. It can be used as: :: sim = Simulator(frag) sim.add_clock(1e-6) with sim.write_vcd("dump.vcd", "dump.gtkw"): sim.run_until(1e-3) Arguments --------- vcd_file : str or file-like object Verilog Value Change Dump file or filename. gtkw_file : str or file-like object GTKWave save file or filename. traces : iterable of Signal Signals to display traces for. """ waveform_writer = _VCDWaveformWriter(self._signal_names, vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces) return _WaveformContextManager(self._state, waveform_writer) # TODO(nmigen-0.3): remove @deprecated("instead of `with Simulator(fragment, ...) as sim:`, use " "`sim = Simulator(fragment); with sim.write_vcd(...):`") def __enter__(self): # :nocov: return self # TODO(nmigen-0.3): remove def __exit__(self, *args): # :nocov: self._state.finish_waveform()