From: whitequark Date: Tue, 11 Dec 2018 20:50:56 +0000 (+0000) Subject: Initial commit. X-Git-Url: https://git.libre-soc.org/?a=commitdiff_plain;h=d4022fa273d29fa20c5a839b4258cdbb7dcbafff;p=nmigen.git Initial commit. --- d4022fa273d29fa20c5a839b4258cdbb7dcbafff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29542a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*.egg-info +*.il +*.v diff --git a/README.md b/README.md new file mode 100644 index 0000000..722c039 --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ +This repository contains a proposal for the design of nMigen in form of an implementation. This implementation deviates from the existing design of Migen by making several observations of its drawbacks: + + * Migen is strongly tailored towards Verilog, yet translation of Migen to Verilog is not straightforward, leaves much semantics implicit (e.g. signedness, width extension, combinatorial assignments, sub-signal assignments...); + * Hierarchical designs are useful for floorplanning and optimization, yet Migen does not support them at all; + * Migen's syntax is not easily composable, and something like an FSM requires extending Migen's syntax in non-orthogonal ways; + * Migen reimplements a lot of mature open-source tooling, such as conversion of RTL to Verilog (Yosys' Verilog backend), or simulation (Icarus Verilog, Verilator, etc.), and often lacks in features, speed, or corner case handling. + * Migen requires awkward specials for some FPGA features such as asynchronous resets. + +It also observes that Yosys' intermediate language, RTLIL, is an ideal target for Migen-style logic, as conversion of FHDL to RTLIL is essentially a 1:1 translation, with the exception of the related issues of naming and hierarchy. + +This proposal makes several major changes to Migen that hopefully solve all of these drawbacks: + + * nMigen changes FHDL's internal representation to closely match that of RTLIL; + * nMigen outputs RTLIL and relies on Yosys for conversion to Verilog, EDIF, etc; + * nMigen uses an exact mapping between FHDL signals and RTLIL names to off-load logic simulation to Icarus Verilog, Verilator, etc; + * nMigen uses an uniform, composable Python eHDL; + * nMigen outputs hierarchical RTLIL, automatically threading signals through the hierarchy; + * nMigen supports asynchronous reset directly; + * nMigen makes driving a signal from multiple clock domains a precise, hard error. + +This proposal keeps in mind but does not make the following major changes: + + * nMigen could be easily modified to flatten the hierarchy if a signal is driven simultaneously from multiple modules; + * nMigen could be easily modified to support `x` values (invalid / don't care) by relying on RTLIL's ability to directly represent them; + * nMigen could be easily modified to support negative edge triggered flip-flops by relying on RTLIL's ability to directly represent them; + * nMigen could be easily modified to track Python source locations of primitives and export them to RTLIL/Verilog through the `src` attribute, displaying the Python source locations in timing reports directly. + +This proposal also makes the following simplifications: + * Specials are eliminated. Primitives such as memory ports are represented directly, and primitives such as tristate buffers are lowered to a selectable implementation via ordinary dependency injection (`f.submodules += platform.get_tristate(triple, io)`). + +The internals of nMigen in this proposal are cleaned up, yet they are kept sufficiently close to Migen that \~all Migen code should be possible to run directly on nMigen using a syntactic compatibility layer. + +FHDL features currently missing from this implementation: + * self.clock_domains += + * Array + * Memory + * Tristate, TSTriple + * Instance + * FSM + * transformers: SplitMemory, FullMemoryWE + * transformers: ClockDomainsRenamer + +`migen.genlib`, `migen.sim` and `migen.build` are missing completely. + +One might reasonably expect that a roundtrip through RTLIL would result in unreadable Verilog. +However, this is not the case, e.g. consider the examples: + +
+alu.v + +```verilog +module \$1 (co, sel, a, b, o); + wire [17:0] _04_; + input [15:0] a; + input [15:0] b; + output co; + reg \co$next ; + output [15:0] o; + reg [15:0] \o$next ; + input [1:0] sel; + assign _04_ = $signed(+ a) + $signed(- b); + always @* begin + \o$next = 16'h0000; + \co$next = 1'h0; + casez ({ 1'h1, sel == 2'h2, sel == 1'h1, sel == 0'b0 }) + 4'bzzz1: + \o$next = a | b; + 4'bzz1z: + \o$next = a & b; + 4'bz1zz: + \o$next = a ^ b; + 4'b1zzz: + { \co$next , \o$next } = _04_[16:0]; + endcase + end + assign o = \o$next ; + assign co = \co$next ; +endmodule +``` +
+ +
+alu_hier.v + +```verilog +module add(b, o, a); + wire [16:0] _0_; + input [15:0] a; + input [15:0] b; + output [15:0] o; + reg [15:0] \o$next ; + assign _0_ = a + b; + always @* begin + \o$next = 16'h0000; + \o$next = _0_[15:0]; + end + assign o = \o$next ; +endmodule + +module sub(b, o, a); + wire [16:0] _0_; + input [15:0] a; + input [15:0] b; + output [15:0] o; + reg [15:0] \o$next ; + assign _0_ = a - b; + always @* begin + \o$next = 16'h0000; + \o$next = _0_[15:0]; + end + assign o = \o$next ; +endmodule + +module top(a, b, o, add_o, sub_o, op); + input [15:0] a; + wire [15:0] add_a; + reg [15:0] \add_a$next ; + wire [15:0] add_b; + reg [15:0] \add_b$next ; + input [15:0] add_o; + input [15:0] b; + output [15:0] o; + reg [15:0] \o$next ; + input op; + wire [15:0] sub_a; + reg [15:0] \sub_a$next ; + wire [15:0] sub_b; + reg [15:0] \sub_b$next ; + input [15:0] sub_o; + add add ( + .a(add_a), + .b(add_b), + .o(add_o) + ); + sub sub ( + .a(sub_a), + .b(sub_b), + .o(sub_o) + ); + always @* begin + \o$next = 16'h0000; + \add_a$next = 16'h0000; + \add_b$next = 16'h0000; + \sub_a$next = 16'h0000; + \sub_b$next = 16'h0000; + \add_a$next = a; + \sub_a$next = a; + \add_b$next = b; + \sub_b$next = b; + casez ({ 1'h1, op }) + 2'bz1: + \o$next = sub_o; + 2'b1z: + \o$next = add_o; + endcase + end + assign o = \o$next ; + assign add_a = \add_a$next ; + assign add_b = \add_b$next ; + assign sub_a = \sub_a$next ; + assign sub_b = \sub_b$next ; +endmodule +``` +
+
+clkdiv.v + +```verilog +module \$1 (sys_clk, o); + wire [16:0] _0_; + output o; + reg \o$next ; + input sys_clk; + wire sys_rst; + (* init = 16'hffff *) + reg [15:0] v = 16'hffff; + reg [15:0] \v$next ; + assign _0_ = v + 1'h1; + always @(posedge sys_clk) + v <= \v$next ; + always @* begin + \o$next = 1'h0; + \v$next = _0_[15:0]; + \o$next = v[15]; + casez (sys_rst) + 1'h1: + \v$next = 16'hffff; + endcase + end + assign o = \o$next ; +endmodule +``` +
+ +
+arst.v + +```verilog +module \$1 (o, sys_clk, sys_rst); + wire [16:0] _0_; + output o; + reg \o$next ; + input sys_clk; + input sys_rst; + (* init = 16'h0000 *) + reg [15:0] v = 16'h0000; + reg [15:0] \v$next ; + assign _0_ = v + 1'h1; + always @(posedge sys_clk or posedge sys_rst) + if (sys_rst) + v <= 16'h0000; + else + v <= \v$next ; + always @* begin + \o$next = 1'h0; + \v$next = _0_[15:0]; + \o$next = v[15]; + end + assign o = \o$next ; +endmodule +``` +
+ +
+pmux.v + +```verilog +module \$1 (c, o, s, a, b); + input [15:0] a; + input [15:0] b; + input [15:0] c; + output [15:0] o; + reg [15:0] \o$next ; + input [2:0] s; + always @* begin + \o$next = 16'h0000; + casez (s) + 3'bzz1: + \o$next = a; + 3'bz1z: + \o$next = b; + 3'b1zz: + \o$next = c; + 3'hz: + \o$next = 16'h0000; + endcase + end + assign o = \o$next ; +endmodule +``` +
diff --git a/examples/alu.py b/examples/alu.py new file mode 100644 index 0000000..1ffb176 --- /dev/null +++ b/examples/alu.py @@ -0,0 +1,29 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class ALU: + def __init__(self, width): + self.sel = Signal(2) + self.a = Signal(width) + self.b = Signal(width) + self.o = Signal(width) + self.co = Signal() + + def get_fragment(self, platform): + f = Module() + with f.If(self.sel == 0b00): + f.comb += self.o.eq(self.a | self.b) + with f.Elif(self.sel == 0b01): + f.comb += self.o.eq(self.a & self.b) + with f.Elif(self.sel == 0b10): + f.comb += self.o.eq(self.a ^ self.b) + with f.Else(): + f.comb += Cat(self.o, self.co).eq(self.a - self.b) + return f.lower(platform) + + +alu = ALU(width=16) +frag = alu.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[alu.sel, alu.a, alu.b, alu.o, alu.co])) +print(verilog.convert(frag, ports=[alu.sel, alu.a, alu.b, alu.o, alu.co])) diff --git a/examples/alu_hier.py b/examples/alu_hier.py new file mode 100644 index 0000000..52d2c19 --- /dev/null +++ b/examples/alu_hier.py @@ -0,0 +1,59 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class Adder: + def __init__(self, width): + self.a = Signal(width) + self.b = Signal(width) + self.o = Signal(width) + + def get_fragment(self, platform): + f = Module() + f.comb += self.o.eq(self.a + self.b) + return f.lower(platform) + + +class Subtractor: + def __init__(self, width): + self.a = Signal(width) + self.b = Signal(width) + self.o = Signal(width) + + def get_fragment(self, platform): + f = Module() + f.comb += self.o.eq(self.a - self.b) + return f.lower(platform) + + +class ALU: + def __init__(self, width): + self.op = Signal() + self.a = Signal(width) + self.b = Signal(width) + self.o = Signal(width) + + self.add = Adder(width) + self.sub = Subtractor(width) + + def get_fragment(self, platform): + f = Module() + f.submodules.add = self.add + f.submodules.sub = self.sub + f.comb += [ + self.add.a.eq(self.a), + self.sub.a.eq(self.a), + self.add.b.eq(self.b), + self.sub.b.eq(self.b), + ] + with f.If(self.op): + f.comb += self.o.eq(self.sub.o) + with f.Else(): + f.comb += self.o.eq(self.add.o) + return f.lower(platform) + + +alu = ALU(width=16) +frag = alu.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[alu.op, alu.a, alu.b, alu.o])) +print(verilog.convert(frag, ports=[alu.op, alu.a, alu.b, alu.o, alu.add.o, alu.sub.o])) diff --git a/examples/arst.py b/examples/arst.py new file mode 100644 index 0000000..73d90fa --- /dev/null +++ b/examples/arst.py @@ -0,0 +1,21 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class ClockDivisor: + def __init__(self, factor): + self.v = Signal(factor) + self.o = Signal() + + def get_fragment(self, platform): + f = Module() + f.sync += self.v.eq(self.v + 1) + f.comb += self.o.eq(self.v[-1]) + return f.lower(platform) + + +sys = ClockDomain(async_reset=True) +ctr = ClockDivisor(factor=16) +frag = ctr.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[sys.clk, sys.rst, ctr.o], clock_domains={"sys": sys})) +print(verilog.convert(frag, ports=[sys.clk, sys.rst, ctr.o], clock_domains={"sys": sys})) diff --git a/examples/clkdiv.py b/examples/clkdiv.py new file mode 100644 index 0000000..f54505e --- /dev/null +++ b/examples/clkdiv.py @@ -0,0 +1,21 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class ClockDivisor: + def __init__(self, factor): + self.v = Signal(factor, reset=2**factor-1) + self.o = Signal() + + def get_fragment(self, platform): + f = Module() + f.sync += self.v.eq(self.v + 1) + f.comb += self.o.eq(self.v[-1]) + return f.lower(platform) + + +sys = ClockDomain() +ctr = ClockDivisor(factor=16) +frag = ctr.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[sys.clk, ctr.o], clock_domains={"sys": sys})) +print(verilog.convert(frag, ports=[sys.clk, ctr.o], clock_domains={"sys": sys})) diff --git a/examples/ctrl.py b/examples/ctrl.py new file mode 100644 index 0000000..9e6d76c --- /dev/null +++ b/examples/ctrl.py @@ -0,0 +1,22 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class ClockDivisor: + def __init__(self, factor): + self.v = Signal(factor, reset=2**factor-1) + self.o = Signal() + self.ce = Signal() + + def get_fragment(self, platform): + f = Module() + f.sync += self.v.eq(self.v + 1) + f.comb += self.o.eq(self.v[-1]) + return CEInserter(self.ce)(f.lower()) + + +sys = ClockDomain() +ctr = ClockDivisor(factor=16) +frag = ctr.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[sys.clk, ctr.o, ctr.ce], clock_domains={"sys": sys})) +print(verilog.convert(frag, ports=[sys.clk, ctr.o, ctr.ce], clock_domains={"sys": sys})) diff --git a/examples/pmux.py b/examples/pmux.py new file mode 100644 index 0000000..be6c0d4 --- /dev/null +++ b/examples/pmux.py @@ -0,0 +1,29 @@ +from nmigen.fhdl import * +from nmigen.back import rtlil, verilog + + +class ParMux: + def __init__(self, width): + self.s = Signal(3) + self.a = Signal(width) + self.b = Signal(width) + self.c = Signal(width) + self.o = Signal(width) + + def get_fragment(self, platform): + f = Module() + with f.Case(self.s, "--1"): + f.comb += self.o.eq(self.a) + with f.Case(self.s, "-1-"): + f.comb += self.o.eq(self.b) + with f.Case(self.s, "1--"): + f.comb += self.o.eq(self.c) + with f.Case(self.s): + f.comb += self.o.eq(0) + return f.lower(platform) + + +pmux = ParMux(width=16) +frag = pmux.get_fragment(platform=None) +# print(rtlil.convert(frag, ports=[pmux.s, pmux.a, pmux.b, pmux.c, pmux.o])) +print(verilog.convert(frag, ports=[pmux.s, pmux.a, pmux.b, pmux.c, pmux.o])) diff --git a/nmigen/back/__init__.py b/nmigen/back/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nmigen/back/rtlil.py b/nmigen/back/rtlil.py new file mode 100644 index 0000000..5d6f7d9 --- /dev/null +++ b/nmigen/back/rtlil.py @@ -0,0 +1,469 @@ +import io +import textwrap +from collections import defaultdict, OrderedDict +from contextlib import contextmanager + +from ..fhdl import ast, ir, xfrm + + +class _Namer: + def __init__(self): + super().__init__() + self._index = 0 + self._names = set() + + def _make_name(self, name, local): + if name is None: + self._index += 1 + name = "${}".format(self._index) + elif not local and name[0] not in "\\$": + name = "\\{}".format(name) + while name in self._names: + self._index += 1 + name = "{}${}".format(name, self._index) + self._names.add(name) + return name + + +class _Bufferer: + def __init__(self): + super().__init__() + self._buffer = io.StringIO() + + def __str__(self): + return self._buffer.getvalue() + + def _append(self, fmt, *args, **kwargs): + self._buffer.write(fmt.format(*args, **kwargs)) + + def _src(self, src): + if src: + self._append(" attribute \\src {}", repr(src)) + + +class _Builder(_Namer, _Bufferer): + def module(self, name=None): + name = self._make_name(name, local=False) + return _ModuleBuilder(self, name) + + +class _ModuleBuilder(_Namer, _Bufferer): + def __init__(self, rtlil, name): + super().__init__() + self.rtlil = rtlil + self.name = name + + def __enter__(self): + self._append("attribute \\generator \"{}\"\n", "nMigen") + self._append("module {}\n", self.name) + return self + + def __exit__(self, *args): + self._append("end\n") + self.rtlil._buffer.write(str(self)) + + def wire(self, width, port_id=None, port_kind=None, name=None, src=""): + self._src(src) + name = self._make_name(name, local=False) + if port_id is None: + self._append(" wire width {} {}\n", width, name) + else: + assert port_kind in ("input", "output", "inout") + self._append(" wire width {} {} {} {}\n", width, port_kind, port_id, name) + return name + + def connect(self, lhs, rhs): + self._append(" connect {} {}\n", lhs, rhs) + + def cell(self, kind, name=None, params={}, ports={}, src=""): + self._src(src) + name = self._make_name(name, local=True) + self._append(" cell {} {}\n", kind, name) + for param, value in params.items(): + if isinstance(value, str): + value = repr(value) + else: + value = int(value) + self._append(" parameter \\{} {}\n", param, value) + for port, wire in ports.items(): + self._append(" connect {} {}\n", port, wire) + self._append(" end\n") + return name + + def process(self, name=None, src=""): + name = self._make_name(name, local=True) + return _ProcessBuilder(self, name, src) + + +class _ProcessBuilder(_Bufferer): + def __init__(self, rtlil, name, src): + super().__init__() + self.rtlil = rtlil + self.name = name + self.src = src + + def __enter__(self): + self._src(self.src) + self._append(" process {}\n", self.name) + return self + + def __exit__(self, *args): + self._append(" end\n") + self.rtlil._buffer.write(str(self)) + + def case(self): + return _CaseBuilder(self, indent=2) + + def sync(self, kind, cond=None): + return _SyncBuilder(self, kind, cond) + + +class _CaseBuilder: + def __init__(self, rtlil, indent): + self.rtlil = rtlil + self.indent = indent + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def assign(self, lhs, rhs): + self.rtlil._append("{}assign {} {}\n", " " * self.indent, lhs, rhs) + + def switch(self, cond): + return _SwitchBuilder(self.rtlil, cond, self.indent) + + +class _SwitchBuilder: + def __init__(self, rtlil, cond, indent): + self.rtlil = rtlil + self.cond = cond + self.indent = indent + + def __enter__(self): + self.rtlil._append("{}switch {}\n", " " * self.indent, self.cond) + return self + + def __exit__(self, *args): + self.rtlil._append("{}end\n", " " * self.indent) + + def case(self, value=None): + if value is None: + self.rtlil._append("{}case\n", " " * (self.indent + 1)) + else: + self.rtlil._append("{}case {}'{}\n", " " * (self.indent + 1), + len(value), value) + return _CaseBuilder(self.rtlil, self.indent + 2) + + +class _SyncBuilder: + def __init__(self, rtlil, kind, cond): + self.rtlil = rtlil + self.kind = kind + self.cond = cond + + def __enter__(self): + if self.cond is None: + self.rtlil._append(" sync {}\n", self.kind) + else: + self.rtlil._append(" sync {} {}\n", self.kind, self.cond) + return self + + def __exit__(self, *args): + pass + + def update(self, lhs, rhs): + self.rtlil._append(" update {} {}\n", lhs, rhs) + + +class _ValueTransformer(xfrm.ValueTransformer): + operator_map = { + (1, "~"): "$not", + (1, "-"): "$neg", + (1, "b"): "$reduce_bool", + (2, "+"): "$add", + (2, "-"): "$sub", + (2, "*"): "$mul", + (2, "/"): "$div", + (2, "%"): "$mod", + (2, "**"): "$pow", + (2, "<<<"): "$sshl", + (2, ">>>"): "$sshr", + (2, "&"): "$and", + (2, "^"): "$xor", + (2, "|"): "$or", + (2, "=="): "$eq", + (2, "!="): "$ne", + (2, "<"): "$lt", + (2, "<="): "$le", + (2, ">"): "$gt", + (2, ">="): "$ge", + (3, "m"): "$mux", + } + + def __init__(self, rtlil): + self.rtlil = rtlil + self.wires = ast.ValueDict() + self.ports = ast.ValueDict() + self.driven = ast.ValueDict() + self.is_lhs = False + self.sub_name = None + + def add_port(self, signal, kind=None): + if signal in self.driven: + self.ports[signal] = (len(self.ports), "output") + else: + self.ports[signal] = (len(self.ports), "input") + + def add_driven(self, signal, sync): + self.driven[signal] = sync + + @contextmanager + def lhs(self): + try: + self.is_lhs = True + yield + finally: + self.is_lhs = False + + @contextmanager + def hierarchy(self, sub_name): + try: + self.sub_name = sub_name + yield + finally: + self.sub_name = None + + def on_unknown(self, node): + if node is None: + return None + else: + super().visit_unknown(node) + + def on_Const(self, node): + if isinstance(node.value, str): + return "{}'{}".format(node.nbits, node.value) + else: + return "{}'{:b}".format(node.nbits, node.value) + + def on_Signal(self, node): + if node in self.wires: + wire_curr, wire_next = self.wires[node] + else: + if node in self.ports: + port_id, port_kind = self.ports[node] + else: + port_id = port_kind = None + if self.sub_name: + wire_name = "{}_{}".format(self.sub_name, node.name) + else: + wire_name = node.name + wire_curr = self.rtlil.wire(width=node.nbits, name=wire_name, + port_id=port_id, port_kind=port_kind) + if node in self.driven: + wire_next = self.rtlil.wire(width=node.nbits, name=wire_curr + "$next") + else: + wire_next = None + self.wires[node] = (wire_curr, wire_next) + + if self.is_lhs: + if wire_next is None: + raise ValueError("Cannot return lhs for non-driven signal {}".format(repr(node))) + return wire_next + else: + return wire_curr + + def on_Operator_unary(self, node): + arg, = node.operands + arg_bits, arg_sign = arg.bits_sign() + res_bits, res_sign = node.bits_sign() + res = self.rtlil.wire(width=res_bits) + self.rtlil.cell(self.operator_map[(1, node.op)], ports={ + "\\A": self(arg), + "\\Y": res, + }, params={ + "A_SIGNED": arg_sign, + "A_WIDTH": arg_bits, + "Y_WIDTH": res_bits, + }) + return res + + def match_bits_sign(self, node, new_bits, new_sign): + if isinstance(node, ast.Const): + return self(ast.Const(node.value, (new_bits, new_sign))) + + node_bits, node_sign = node.bits_sign() + if new_bits > node_bits: + res = self.rtlil.wire(width=new_bits) + self.rtlil.cell("$pos", ports={ + "\\A": self(node), + "\\Y": res, + }, params={ + "A_SIGNED": node_sign, + "A_WIDTH": node_bits, + "Y_WIDTH": new_bits, + }) + return res + else: + return "{} [{}:0]".format(self(node), new_bits - 1) + + def on_Operator_binary(self, node): + lhs, rhs = node.operands + lhs_bits, lhs_sign = lhs.bits_sign() + rhs_bits, rhs_sign = rhs.bits_sign() + if lhs_sign == rhs_sign: + lhs_wire = self(lhs) + rhs_wire = self(rhs) + else: + lhs_sign = rhs_sign = True + lhs_bits = rhs_bits = max(lhs_bits, rhs_bits) + lhs_wire = self.match_bits_sign(lhs, lhs_bits, lhs_sign) + rhs_wire = self.match_bits_sign(rhs, rhs_bits, rhs_sign) + res_bits, res_sign = node.bits_sign() + res = self.rtlil.wire(width=res_bits) + self.rtlil.cell(self.operator_map[(2, node.op)], ports={ + "\\A": lhs_wire, + "\\B": rhs_wire, + "\\Y": res, + }, params={ + "A_SIGNED": lhs_sign, + "A_WIDTH": lhs_bits, + "B_SIGNED": rhs_sign, + "B_WIDTH": rhs_bits, + "Y_WIDTH": res_bits, + }) + return res + + def on_Operator_mux(self, node): + sel, lhs, rhs = node.operands + lhs_bits, lhs_sign = lhs.bits_sign() + rhs_bits, rhs_sign = rhs.bits_sign() + res_bits, res_sign = node.bits_sign() + res = self.rtlil.wire(width=res_bits) + self.rtlil.cell("$mux", ports={ + "\\A": self(lhs), + "\\B": self(rhs), + "\\S": self(sel), + "\\Y": res, + }, params={ + "WIDTH": max(lhs_bits, rhs_bits, res_bits) + }) + return res + + def on_Operator(self, node): + if len(node.operands) == 1: + return self.on_Operator_unary(node) + elif len(node.operands) == 2: + return self.on_Operator_binary(node) + elif len(node.operands) == 3: + assert node.op == "m" + return self.on_Operator_mux(node) + else: + raise TypeError + + def on_Slice(self, node): + if node.end == node.start + 1: + return "{} [{}]".format(self(node.value), node.start) + else: + return "{} [{}:{}]".format(self(node.value), node.end - 1, node.start) + + # def on_Part(self, node): + # return _Part(self(node.value), self(node.offset), node.width) + + def on_Cat(self, node): + return "{{ {} }}".format(" ".join(reversed([self(o) for o in node.operands]))) + + def on_Repl(self, node): + return "{{ {} }}".format(" ".join(self(node.value) for _ in range(node.count))) + + +def convert_fragment(builder, fragment, name, clock_domains): + with builder.module(name) as module: + xformer = _ValueTransformer(module) + + for cd_name, signal in fragment.iter_drivers(): + xformer.add_driven(signal, sync=cd_name is not None) + + for signal in fragment.ports: + xformer.add_port(signal) + + for subfragment, sub_name in fragment.subfragments: + sub_name, sub_port_map = \ + convert_fragment(builder, subfragment, sub_name, clock_domains) + with xformer.hierarchy(sub_name): + module.cell(sub_name, name=sub_name, ports={ + p: xformer(s) for p, s in sub_port_map.items() + }) + + with module.process() as process: + with process.case() as case: + for cd_name, signal in fragment.iter_drivers(): + if cd_name is None: + prev_value = xformer(ast.Const(signal.reset, signal.nbits)) + else: + prev_value = xformer(signal) + with xformer.lhs(): + case.assign(xformer(signal), prev_value) + + def _convert_stmts(case, stmts): + for stmt in stmts: + if isinstance(stmt, ast.Assign): + lhs_bits, lhs_sign = stmt.lhs.bits_sign() + rhs_bits, rhs_sign = stmt.rhs.bits_sign() + if lhs_bits == rhs_bits: + rhs_sigspec = xformer(stmt.rhs) + else: + rhs_sigspec = xformer.match_bits_sign( + stmt.rhs, lhs_bits, rhs_sign) + with xformer.lhs(): + lhs_sigspec = xformer(stmt.lhs) + case.assign(lhs_sigspec, rhs_sigspec) + + elif isinstance(stmt, ast.Switch): + with case.switch(xformer(stmt.test)) as switch: + for value, nested_stmts in stmt.cases.items(): + with switch.case(value) as nested_case: + _convert_stmts(nested_case, nested_stmts) + + else: + raise TypeError + + _convert_stmts(case, fragment.statements) + + with process.sync("init") as sync: + for cd_name, signal in fragment.iter_sync(): + sync.update(xformer(signal), + xformer(ast.Const(signal.reset, signal.nbits))) + + for cd_name, signals in fragment.iter_domains(): + triggers = [] + if cd_name is None: + triggers.append(("always",)) + else: + cd = clock_domains[cd_name] + triggers.append(("posedge", xformer(cd.clk))) + if cd.async_reset: + triggers.append(("posedge", xformer(cd.rst))) + + for trigger in triggers: + with process.sync(*trigger) as sync: + for signal in signals: + xformer(signal) + wire_curr, wire_next = xformer.wires[signal] + sync.update(wire_curr, wire_next) + + port_map = OrderedDict() + for signal in fragment.ports: + port_map[xformer(signal)] = signal + + return module.name, port_map + + +def convert(fragment, ports=[], clock_domains={}): + fragment, ins, outs = fragment.prepare(ports, clock_domains) + + builder = _Builder() + convert_fragment(builder, fragment, "top", clock_domains) + return str(builder) diff --git a/nmigen/back/verilog.py b/nmigen/back/verilog.py new file mode 100644 index 0000000..90a5c2f --- /dev/null +++ b/nmigen/back/verilog.py @@ -0,0 +1,34 @@ +import os +import subprocess + +from . import rtlil + + +__all__ = ["convert"] + + +class YosysError(Exception): + pass + + +def convert(*args, **kwargs): + il_text = rtlil.convert(*args, **kwargs) + popen = subprocess.Popen([os.getenv("YOSYS", "yosys"), "-q", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8") + verilog_text, error = popen.communicate(""" +read_ilang <>>", [self, other]) + def __rrshift__(self, other): + return Operator(">>>", [other, self]) + def __and__(self, other): + return Operator("&", [self, other]) + def __rand__(self, other): + return Operator("&", [other, self]) + def __xor__(self, other): + return Operator("^", [self, other]) + def __rxor__(self, other): + return Operator("^", [other, self]) + def __or__(self, other): + return Operator("|", [self, other]) + def __ror__(self, other): + return Operator("|", [other, self]) + + def __eq__(self, other): + return Operator("==", [self, other]) + def __ne__(self, other): + return Operator("!=", [self, other]) + def __lt__(self, other): + return Operator("<", [self, other]) + def __le__(self, other): + return Operator("<=", [self, other]) + def __gt__(self, other): + return Operator(">", [self, other]) + def __ge__(self, other): + return Operator(">=", [self, other]) + + def __len__(self): + return self.bits_sign()[0] + + def __getitem__(self, key): + n = len(self) + if isinstance(key, int): + if key not in range(-n, n): + raise IndexError("Cannot index {} bits into {}-bit value".format(key, n)) + if key < 0: + key += n + return Slice(self, key, key + 1) + elif isinstance(key, slice): + start, stop, step = key.indices(n) + if step != 1: + return Cat(self[i] for i in range(start, stop, step)) + return Slice(self, start, stop) + else: + raise TypeError("Cannot index value with {}".format(repr(key))) + + def bool(self): + """Conversion to boolean. + + Returns + ------- + Value, out + Output ``Value``. If any bits are set, returns ``1``, else ``0``. + """ + return Operator("b", [self]) + + def part(self, offset, width): + """Indexed part-select. + + Selects a constant width but variable offset part of a ``Value``. + + Parameters + ---------- + offset : Value, in + start point of the selected bits + width : int + number of selected bits + + Returns + ------- + Part, out + Selected part of the ``Value`` + """ + return Part(self, offset, width) + + def eq(self, value): + """Assignment. + + Parameters + ---------- + value : Value, in + Value to be assigned. + + Returns + ------- + Assign + Assignment statement that can be used in combinatorial or synchronous context. + """ + return Assign(self, value) + + def bits_sign(self): + """Bit length and signedness of a value. + + Returns + ------- + int, bool + Number of bits required to store `v` or available in `v`, followed by + whether `v` has a sign bit (included in the bit count). + + Examples + -------- + >>> Value.bits_sign(Signal(8)) + 8, False + >>> Value.bits_sign(C(0xaa)) + 8, False + """ + raise TypeError("Cannot calculate bit length of {!r}".format(self)) + + def _lhs_signals(self): + raise TypeError("Value {!r} cannot be used in assignments".format(self)) + + def _rhs_signals(self): + raise NotImplementedError + + def __hash__(self): + raise TypeError("Unhashable type: {}".format(type(self).__name__)) + + +class Const(Value): + """A constant, literal integer value. + + Parameters + ---------- + value : int + bits_sign : int or tuple or None + Either an integer `bits` or a tuple `(bits, signed)` + specifying the number of bits in this `Const` and whether it is + signed (can represent negative values). `bits_sign` defaults + to the minimum width and signedness of `value`. + + Attributes + ---------- + nbits : int + signed : bool + """ + def __init__(self, value, bits_sign=None): + self.value = int(value) + if bits_sign is None: + bits_sign = self.value.bit_length(), self.value < 0 + if isinstance(bits_sign, int): + bits_sign = bits_sign, self.value < 0 + self.nbits, self.signed = bits_sign + if not isinstance(self.nbits, int) or self.nbits < 0: + raise TypeError("Width must be a positive integer") + + def bits_sign(self): + return self.nbits, self.signed + + def _rhs_signals(self): + return ValueSet() + + def __eq__(self, other): + return self.value == other.value + + def __hash__(self): + return hash(self.value) + + def __repr__(self): + return "(const {}'{}d{})".format(self.nbits, "s" if self.signed else "", self.value) + + +C = Const # shorthand + + +class Operator(Value): + def __init__(self, op, operands): + super().__init__() + self.op = op + self.operands = [Value.wrap(o) for o in operands] + + @staticmethod + def _bitwise_binary_bits_sign(a, b): + if not a[1] and not b[1]: + # both operands unsigned + return max(a[0], b[0]), False + elif a[1] and b[1]: + # both operands signed + return max(a[0], b[0]), True + elif not a[1] and b[1]: + # first operand unsigned (add sign bit), second operand signed + return max(a[0] + 1, b[0]), True + else: + # first signed, second operand unsigned (add sign bit) + return max(a[0], b[0] + 1), True + + def bits_sign(self): + obs = list(map(lambda x: x.bits_sign(), self.operands)) + if self.op == "+" or self.op == "-": + if len(obs) == 1: + if self.op == "-" and not obs[0][1]: + return obs[0][0] + 1, True + else: + return obs[0] + n, s = self._bitwise_binary_bits_sign(*obs) + return n + 1, s + elif self.op == "*": + if not obs[0][1] and not obs[1][1]: + # both operands unsigned + return obs[0][0] + obs[1][0], False + elif obs[0][1] and obs[1][1]: + # both operands signed + return obs[0][0] + obs[1][0] - 1, True + else: + # one operand signed, the other unsigned (add sign bit) + return obs[0][0] + obs[1][0] + 1 - 1, True + elif self.op == "<<<": + if obs[1][1]: + extra = 2**(obs[1][0] - 1) - 1 + else: + extra = 2**obs[1][0] - 1 + return obs[0][0] + extra, obs[0][1] + elif self.op == ">>>": + if obs[1][1]: + extra = 2**(obs[1][0] - 1) + else: + extra = 0 + return obs[0][0] + extra, obs[0][1] + elif self.op == "&" or self.op == "^" or self.op == "|": + return self._bitwise_binary_bits_sign(*obs) + elif (self.op == "<" or self.op == "<=" or self.op == "==" or self.op == "!=" or + self.op == ">" or self.op == ">="): + return 1, False + elif self.op == "~": + return obs[0] + elif self.op == "m": + return _bitwise_binary_bits_sign(obs[1], obs[2]) + else: + raise TypeError + + def _rhs_signals(self): + return union(op._rhs_signals() for op in self.operands) + + def __repr__(self): + if len(self.operands) == 1: + return "({} {})".format(self.op, self.operands[0]) + elif len(self.operands) == 2: + return "({} {} {})".format(self.op, self.operands[0], self.operands[1]) + + +def Mux(sel, val1, val0): + """Choose between two values. + + Parameters + ---------- + sel : Value, in + Selector. + val1 : Value, in + val0 : Value, in + Input values. + + Returns + ------- + Value, out + Output ``Value``. If ``sel`` is asserted, the Mux returns ``val1``, else ``val0``. + """ + return Operator("m", [sel, val1, val0]) + + +class Slice(Value): + def __init__(self, value, start, end): + if not isinstance(start, int): + raise TypeError("Slice start must be integer, not {!r}".format(start)) + if not isinstance(end, int): + raise TypeError("Slice end must be integer, not {!r}".format(end)) + + n = len(value) + if start not in range(-n, n): + raise IndexError("Cannot start slice {} bits into {}-bit value".format(start, n)) + if start < 0: + start += n + if end not in range(-(n+1), n+1): + raise IndexError("Cannot end slice {} bits into {}-bit value".format(end, n)) + if end < 0: + end += n + + super().__init__() + self.value = Value.wrap(value) + self.start = start + self.end = end + + def bits_sign(self): + return self.end - self.start, False + + def _lhs_signals(self): + return self.value._lhs_signals() + + def _rhs_signals(self): + return self.value._rhs_signals() + + def __repr__(self): + return "(slice {} {}:{})".format(repr(self.value), self.start, self.end) + + +class Part(Value): + def __init__(self, value, offset, width): + if not isinstance(width, int) or width < 0: + raise TypeError("Part width must be a positive integer, not {!r}".format(width)) + + super().__init__() + self.value = value + self.offset = Value.wrap(offset) + self.width = width + + def bits_sign(self): + return self.width, False + + def _lhs_signals(self): + return self.value._lhs_signals() + + def _rhs_signals(self): + return self.value._rhs_signals() + + def __repr__(self): + return "(part {} {})".format(repr(self.value), repr(self.offset), self.width) + + +class Cat(Value): + """Concatenate values. + + Form a compound ``Value`` from several smaller ones by concatenation. + The first argument occupies the lower bits of the result. + The return value can be used on either side of an assignment, that + is, the concatenated value can be used as an argument on the RHS or + as a target on the LHS. If it is used on the LHS, it must solely + consist of ``Signal`` s, slices of ``Signal`` s, and other concatenations + meeting these properties. The bit length of the return value is the sum of + the bit lengths of the arguments:: + + len(Cat(args)) == sum(len(arg) for arg in args) + + Parameters + ---------- + *args : Values or iterables of Values, inout + ``Value`` s to be concatenated. + + Returns + ------- + Value, inout + Resulting ``Value`` obtained by concatentation. + """ + def __init__(self, *args): + super().__init__() + self.operands = [Value.wrap(v) for v in flatten(args)] + + def bits_sign(self): + return sum(len(op) for op in self.operands), False + + def _lhs_signals(self): + return union(op._lhs_signals() for op in self.operands) + + def _rhs_signals(self): + return union(op._rhs_signals() for op in self.operands) + + def __repr__(self): + return "(cat {})".format(" ".join(map(repr, self.operands))) + + +class Repl(Value): + """Replicate a value + + An input value is replicated (repeated) several times + to be used on the RHS of assignments:: + + len(Repl(s, n)) == len(s) * n + + Parameters + ---------- + value : Value, in + Input value to be replicated. + count : int + Number of replications. + + Returns + ------- + Repl, out + Replicated value. + """ + def __init__(self, value, count): + if not isinstance(count, int) or count < 0: + raise TypeError("Replication count must be a positive integer, not {!r}".format(count)) + + super().__init__() + self.value = Value.wrap(value) + self.count = count + + def bits_sign(self): + return len(self.value) * self.count, False + + def _rhs_signals(self): + return value._rhs_signals() + + +class Signal(Value, DUID): + """A varying integer value. + + Parameters + ---------- + bits_sign : int or tuple or None + Either an integer ``bits`` or a tuple ``(bits, signed)`` specifying the number of bits + in this ``Signal`` and whether it is signed (can represent negative values). + ``bits_sign`` defaults to 1-bit and non-signed. + name : str + Name hint for this signal. If ``None`` (default) the name is inferred from the variable + name this ``Signal`` is assigned to. Name collisions are automatically resolved by + prepending names of objects that contain this ``Signal`` and by appending integer + sequences. + reset : int + Reset (synchronous) or default (combinatorial) value. + When this ``Signal`` is assigned to in synchronous context and the corresponding clock + domain is reset, the ``Signal`` assumes the given value. When this ``Signal`` is unassigned + in combinatorial context (due to conditional assignments not being taken), the ``Signal`` + assumes its ``reset`` value. Defaults to 0. + + Attributes + ---------- + nbits : int + signed : bool + name : str + reset : int + """ + + def __init__(self, bits_sign=1, reset=0, name=None): + super().__init__() + + if name is None: + name = tracer.get_var_name() + self.name = name + + if isinstance(bits_sign, int): + bits_sign = bits_sign, False + self.nbits, self.signed = bits_sign + if not isinstance(self.nbits, int) or self.nbits < 0: + raise TypeError("Width must be a positive integer") + self.reset = reset + + def bits_sign(self): + return self.nbits, self.signed + + def _lhs_signals(self): + return ValueSet((self,)) + + def _rhs_signals(self): + return ValueSet((self,)) + + def __repr__(self): + return "(sig {})".format(self.name) + + +class ClockSignal(Value): + """Clock signal for a given clock domain. + + ``ClockSignal`` s for a given clock domain can be retrieved multiple + times. They all ultimately refer to the same signal. + + Parameters + ---------- + cd : str + Clock domain to obtain a clock signal for. Defaults to `"sys"`. + """ + def __init__(self, cd="sys"): + super().__init__() + if not isinstance(cd, str): + raise TypeError("Clock domain name must be a string, not {!r}".format(cd)) + self.cd = cd + + def __repr__(self): + return "(clk {})".format(self.cd) + + +class ResetSignal(Value): + """Reset signal for a given clock domain + + `ResetSignal` s for a given clock domain can be retrieved multiple + times. They all ultimately refer to the same signal. + + Parameters + ---------- + cd : str + Clock domain to obtain a reset signal for. Defaults to `"sys"`. + """ + def __init__(self, cd="sys"): + super().__init__() + if not isinstance(cd, str): + raise TypeError("Clock domain name must be a string, not {!r}".format(cd)) + self.cd = cd + + def __repr__(self): + return "(rst {})".format(self.cd) + + +class Statement: + @staticmethod + def wrap(obj): + if isinstance(obj, Iterable): + return sum((Statement.wrap(e) for e in obj), []) + else: + if isinstance(obj, Statement): + return [obj] + else: + raise TypeError("Object {!r} is not a Migen statement".format(obj)) + + +class Assign(Statement): + def __init__(self, lhs, rhs): + self.lhs = Value.wrap(lhs) + self.rhs = Value.wrap(rhs) + + def _lhs_signals(self): + return self.lhs._lhs_signals() + + def _rhs_signals(self): + return self.rhs._rhs_signals() + + def __repr__(self): + return "(eq {!r} {!r})".format(self.lhs, self.rhs) + + +class Switch(Statement): + def __init__(self, test, cases): + self.test = Value.wrap(test) + self.cases = OrderedDict() + for key, stmts in cases.items(): + if isinstance(key, (bool, int)): + key = "{:0{}b}".format(key, len(test)) + elif isinstance(key, str): + assert len(key) == len(test) + else: + raise TypeError + if not isinstance(stmts, Iterable): + stmts = [stmts] + self.cases[key] = Statement.wrap(stmts) + + def _lhs_signals(self): + return union(s._lhs_signals() for ss in self.cases.values() for s in ss ) + + def _rhs_signals(self): + signals = union(s._rhs_signals() for ss in self.cases.values() for s in ss) + return self.test._rhs_signals() | signals + + def __repr__(self): + cases = ["(case {} {})".format(key, " ".join(map(repr, stmts))) + for key, stmts in self.cases.items()] + return "(switch {!r} {})".format(self.test, " ".join(cases)) + + +class ValueKey: + def __init__(self, value): + self.value = Value.wrap(value) + + def __hash__(self): + if isinstance(self.value, Const): + return hash(self.value) + elif isinstance(self.value, Signal): + return hash(id(self.value)) + elif isinstance(self.value, Slice): + return hash((ValueKey(self.value.value), self.value.start, self.value.end)) + else: + raise TypeError + + def __eq__(self, other): + if not isinstance(other, ValueKey): + return False + if type(self.value) != type(other.value): + return False + + if isinstance(self.value, Const): + return self.value == other.value + elif isinstance(self.value, Signal): + return id(self.value) == id(other.value) + elif isinstance(self.value, Slice): + return (ValueKey(self.value.value) == ValueKey(other.value.value) and + self.value.start == other.value.start and + self.value.end == other.value.end) + else: + raise TypeError + + def __lt__(self, other): + if not isinstance(other, ValueKey): + return False + if type(self.value) != type(other.value): + return False + + if isinstance(self.value, Const): + return self.value < other.value + elif isinstance(self.value, Signal): + return self.value.duid < other.value.duid + elif isinstance(self.value, Slice): + return (ValueKey(self.value.value) < ValueKey(other.value.value) and + self.value.start < other.value.start and + self.value.end < other.value.end) + else: + raise TypeError + + +class ValueDict(MutableMapping): + def __init__(self, pairs=()): + self._inner = dict() + for key, value in pairs: + self[key] = value + + def __getitem__(self, key): + key = None if key is None else ValueKey(key) + return self._inner[key] + + def __setitem__(self, key, value): + key = None if key is None else ValueKey(key) + self._inner[key] = value + + def __delitem__(self, key): + key = None if key is None else ValueKey(key) + del self._inner[key] + + def __iter__(self): + return map(lambda x: None if x is None else x.value, sorted(self._inner)) + + def __len__(self): + return len(self._inner) + + +class ValueSet(MutableSet): + def __init__(self, elements=()): + self._inner = set() + for elem in elements: + self.add(elem) + + def add(self, value): + self._inner.add(ValueKey(value)) + + def update(self, values): + for value in values: + self.add(value) + + def discard(self, value): + self._inner.discard(ValueKey(value)) + + def __contains__(self, value): + return ValueKey(value) in self._inner + + def __iter__(self): + return map(lambda x: x.value, sorted(self._inner)) + + def __len__(self): + return len(self._inner) + + def __repr__(self): + return "ValueSet({})".format(", ".join(repr(x) for x in self)) diff --git a/nmigen/fhdl/cd.py b/nmigen/fhdl/cd.py new file mode 100644 index 0000000..5e21220 --- /dev/null +++ b/nmigen/fhdl/cd.py @@ -0,0 +1,48 @@ +from .. import tracer +from .ast import Signal + + +__all__ = ["ClockDomain"] + + +class ClockDomain: + """Synchronous domain. + + Parameters + ---------- + name : str or None + Domain name. If ``None`` (the default) the name is inferred from the variable name this + ``ClockDomain`` is assigned to (stripping any `"cd_"` prefix). + reset_less : bool + If ``True``, the domain does not use a reset signal. Registers within this domain are + still all initialized to their reset state once, e.g. through Verilog `"initial"` + statements. + async_reset : bool + If ``True``, the domain uses an asynchronous reset, and registers within this domain + are initialized to their reset state when reset level changes. Otherwise, registers + are initialized to reset state at the next clock cycle when reset is asserted. + + Attributes + ---------- + clk : Signal, inout + The clock for this domain. Can be driven or used to drive other signals (preferably + in combinatorial context). + rst : Signal or None, inout + Reset signal for this domain. Can be driven or used to drive. + """ + def __init__(self, name=None, reset_less=False, async_reset=False): + if name is None: + name = tracer.get_var_name() + if name is None: + raise ValueError("Clock domain name must be specified explicitly") + if name.startswith("cd_"): + name = name[3:] + self.name = name + + self.clk = Signal(name=self.name + "_clk") + if reset_less: + self.rst = None + else: + self.rst = Signal(name=self.name + "_rst") + + self.async_reset = async_reset diff --git a/nmigen/fhdl/dsl.py b/nmigen/fhdl/dsl.py new file mode 100644 index 0000000..b193e12 --- /dev/null +++ b/nmigen/fhdl/dsl.py @@ -0,0 +1,240 @@ +from collections import OrderedDict + +from .ast import * +from .ir import * +from .xfrm import * + + +__all__ = ["Module"] + + +class _ModuleBuilderProxy: + def __init__(self, builder, depth): + object.__setattr__(self, "_builder", builder) + object.__setattr__(self, "_depth", depth) + + +class _ModuleBuilderComb(_ModuleBuilderProxy): + def __iadd__(self, assigns): + self._builder._add_statement(assigns, cd=None, depth=self._depth) + return self + + +class _ModuleBuilderSyncCD(_ModuleBuilderProxy): + def __init__(self, builder, depth, cd): + super().__init__(builder, depth) + self._cd = cd + + def __iadd__(self, assigns): + self._builder._add_statement(assigns, cd=self._cd, depth=self._depth) + return self + + +class _ModuleBuilderSync(_ModuleBuilderProxy): + def __iadd__(self, assigns): + self._builder._add_statement(assigns, cd="sys", depth=self._depth) + return self + + def __getattr__(self, name): + return _ModuleBuilderSyncCD(self._builder, self._depth, name) + + def __setattr__(self, name, value): + if not isinstance(value, _ModuleBuilderSyncCD): + raise AttributeError("Cannot assign sync.{} attribute - use += instead" + .format(name)) + + +class _ModuleBuilderRoot: + def __init__(self, builder, depth): + self._builder = builder + self.comb = _ModuleBuilderComb(builder, depth) + self.sync = _ModuleBuilderSync(builder, depth) + + def __setattr__(self, name, value): + if name == "comb" and not isinstance(value, _ModuleBuilderComb): + raise AttributeError("Cannot assign comb attribute - use += instead") + if name == "sync" and not isinstance(value, _ModuleBuilderSync): + raise AttributeError("Cannot assign sync attribute - use += instead") + super().__setattr__(name, value) + + +class _ModuleBuilderIf(_ModuleBuilderRoot): + def __init__(self, builder, depth, cond): + super().__init__(builder, depth) + self._cond = cond + + def __enter__(self): + self._builder._flush() + self._builder._stmt_if_cond.append(self._cond) + self._outer_case = self._builder._statements + self._builder._statements = [] + return self + + def __exit__(self, *args): + self._builder._stmt_if_bodies.append(self._builder._statements) + self._builder._statements = self._outer_case + + +class _ModuleBuilderElif(_ModuleBuilderRoot): + def __init__(self, builder, depth, cond): + super().__init__(builder, depth) + self._cond = cond + + def __enter__(self): + if not self._builder._stmt_if_cond: + raise ValueError("Elif without preceding If") + self._builder._stmt_if_cond.append(self._cond) + self._outer_case = self._builder._statements + self._builder._statements = [] + return self + + def __exit__(self, *args): + self._builder._stmt_if_bodies.append(self._builder._statements) + self._builder._statements = self._outer_case + + +class _ModuleBuilderElse(_ModuleBuilderRoot): + def __init__(self, builder, depth): + super().__init__(builder, depth) + + def __enter__(self): + if not self._builder._stmt_if_cond: + raise ValueError("Else without preceding If/Elif") + self._builder._stmt_if_cond.append(1) + self._outer_case = self._builder._statements + self._builder._statements = [] + return self + + def __exit__(self, *args): + self._builder._stmt_if_bodies.append(self._builder._statements) + self._builder._statements = self._outer_case + self._builder._flush() + + +class _ModuleBuilderCase(_ModuleBuilderRoot): + def __init__(self, builder, depth, test, value): + super().__init__(builder, depth) + self._test = test + self._value = value + + def __enter__(self): + if self._value is None: + self._value = "-" * len(self._test) + if isinstance(self._value, str) and len(self._test) != len(self._value): + raise ValueError("Case value {} must have the same width as test {}" + .format(self._value, self._test)) + if self._builder._stmt_switch_test != ValueKey(self._test): + self._builder._flush() + self._builder._stmt_switch_test = ValueKey(self._test) + self._outer_case = self._builder._statements + self._builder._statements = [] + return self + + def __exit__(self, *args): + self._builder._stmt_switch_cases[self._value] = self._builder._statements + self._builder._statements = self._outer_case + + +class _ModuleBuilderSubmodules: + def __init__(self, builder): + object.__setattr__(self, "_builder", builder) + + def __iadd__(self, submodules): + for submodule in submodules: + self._builder._add_submodule(submodule) + return self + + def __setattr__(self, name, submodule): + self._builder._add_submodule(submodule, name) + + +class Module(_ModuleBuilderRoot): + def __init__(self): + _ModuleBuilderRoot.__init__(self, self, depth=0) + self.submodules = _ModuleBuilderSubmodules(self) + + self._submodules = [] + self._driving = ValueDict() + self._statements = [] + self._stmt_depth = 0 + self._stmt_if_cond = [] + self._stmt_if_bodies = [] + self._stmt_switch_test = None + self._stmt_switch_cases = OrderedDict() + + def If(self, cond): + return _ModuleBuilderIf(self, self._stmt_depth + 1, cond) + + def Elif(self, cond): + return _ModuleBuilderElif(self, self._stmt_depth + 1, cond) + + def Else(self): + return _ModuleBuilderElse(self, self._stmt_depth + 1) + + def Case(self, test, value=None): + return _ModuleBuilderCase(self, self._stmt_depth + 1, test, value) + + def _flush(self): + if self._stmt_if_cond: + tests, cases = [], OrderedDict() + for if_cond, if_case in zip(self._stmt_if_cond, self._stmt_if_bodies): + if_cond = Value.wrap(if_cond) + if len(if_cond) != 1: + if_cond = if_cond.bool() + tests.append(if_cond) + + match = ("1" + "-" * (len(tests) - 1)).rjust(len(self._stmt_if_cond), "-") + cases[match] = if_case + self._statements.append(Switch(Cat(tests), cases)) + + if self._stmt_switch_test: + self._statements.append(Switch(self._stmt_switch_test.value, self._stmt_switch_cases)) + + self._stmt_if_cond = [] + self._stmt_if_bodies = [] + self._stmt_switch_test = None + self._stmt_switch_cases = OrderedDict() + + def _add_statement(self, assigns, cd, depth): + def cd_name(cd): + if cd is None: + return "comb" + else: + return "sync.{}".format(cd) + + if depth < self._stmt_depth: + self._flush() + self._stmt_depth = depth + + for assign in Statement.wrap(assigns): + if not isinstance(assign, Assign): + raise TypeError("Only assignments can be appended to {}".format(self.cd_name(cd))) + + for signal in assign.lhs._lhs_signals(): + if signal not in self._driving: + self._driving[signal] = cd + elif self._driving[signal] != cd: + cd_curr = self._driving[signal] + raise ValueError("Driver-driver conflict: trying to drive {!r} from {}, but " + "it is already driven from {}" + .format(signal, self.cd_name(cd), self.cd_name(cd_curr))) + + self._statements.append(assign) + + def _add_submodule(self, submodule, name=None): + if not hasattr(submodule, "get_fragment"): + raise TypeError("Trying to add {!r}, which does not have .get_fragment(), as " + " a submodule") + self._submodules.append((submodule, name)) + + def lower(self, platform): + self._flush() + + fragment = Fragment() + for submodule, name in self._submodules: + fragment.add_subfragment(submodule.get_fragment(platform), name) + fragment.add_statements(self._statements) + for signal, cd_name in self._driving.items(): + for lhs_signal in signal._lhs_signals(): + fragment.drive(lhs_signal, cd_name) + return fragment diff --git a/nmigen/fhdl/ir.py b/nmigen/fhdl/ir.py new file mode 100644 index 0000000..5a00cc4 --- /dev/null +++ b/nmigen/fhdl/ir.py @@ -0,0 +1,74 @@ +from collections import defaultdict, OrderedDict + +from ..tools import * +from .ast import * + + +__all__ = ["Fragment"] + + +class Fragment: + def __init__(self): + self.ports = ValueSet() + self.drivers = OrderedDict() + self.statements = [] + self.subfragments = [] + + def add_ports(self, *ports): + self.ports.update(flatten(ports)) + + def iter_ports(self): + yield from self.ports + + def drive(self, signal, cd_name=None): + if cd_name not in self.drivers: + self.drivers[cd_name] = ValueSet() + self.drivers[cd_name].add(signal) + + def iter_domains(self): + yield from self.drivers.items() + + def iter_drivers(self): + for cd_name, signals in self.drivers.items(): + for signal in signals: + yield cd_name, signal + + def iter_comb(self): + yield from self.drivers[None] + + def iter_sync(self): + for cd_name, signals in self.drivers.items(): + if cd_name is None: + continue + for signal in signals: + yield cd_name, signal + + def add_statements(self, *stmts): + self.statements += Statement.wrap(stmts) + + def add_subfragment(self, subfragment, name=None): + assert isinstance(subfragment, Fragment) + self.subfragments.append((subfragment, name)) + + def prepare(self, ports, clock_domains): + from .xfrm import ResetInserter + + resets = {cd.name: cd.rst for cd in clock_domains.values() if cd.rst is not None} + frag = ResetInserter(resets)(self) + + self_driven = union(s._lhs_signals() for s in self.statements) + self_used = union(s._rhs_signals() for s in self.statements) + + ins = self_used - self_driven + outs = ports & self_driven + + for n, (subfrag, name) in enumerate(frag.subfragments): + subfrag, sub_ins, sub_outs = subfrag.prepare(ports=self_used | ports, + clock_domains=clock_domains) + frag.subfragments[n] = (subfrag, name) + ins |= sub_ins - self_driven + outs |= ports & sub_outs + + frag.add_ports(ins, outs) + + return frag, ins, outs diff --git a/nmigen/fhdl/xfrm.py b/nmigen/fhdl/xfrm.py new file mode 100644 index 0000000..0e95ec4 --- /dev/null +++ b/nmigen/fhdl/xfrm.py @@ -0,0 +1,124 @@ +from collections import OrderedDict + +from .ast import * +from .ir import * + + +__all__ = ["ValueTransformer", "StatementTransformer", "ResetInserter", "CEInserter"] + + +class ValueTransformer: + def on_Const(self, value): + return value + + def on_Signal(self, value): + return value + + def on_ClockSignal(self, value): + return value + + def on_ResetSignal(self, value): + return value + + def on_Operator(self, value): + return Operator(value.op, [self.on_value(o) for o in value.operands]) + + def on_Slice(self, value): + return Slice(self.on_value(value.value), value.start, value.end) + + def on_Part(self, value): + return Part(self.on_value(value.value), self.on_value(value.offset), value.width) + + def on_Cat(self, value): + return Cat(self.on_value(o) for o in value.operands) + + def on_Repl(self, value): + return Repl(self.on_value(value.value), value.count) + + def on_value(self, value): + if isinstance(value, Const): + return self.on_Const(value) + elif isinstance(value, Signal): + return self.on_Signal(value) + elif isinstance(value, ClockSignal): + return self.on_ClockSignal(value) + elif isinstance(value, ResetSignal): + return self.on_ResetSignal(value) + elif isinstance(value, Operator): + return self.on_Operator(value) + elif isinstance(value, Slice): + return self.on_Slice(value) + elif isinstance(value, Part): + return self.on_Part(value) + elif isinstance(value, Cat): + return self.on_Cat(value) + elif isinstance(value, Repl): + return self.on_Repl(value) + else: + raise TypeError("Cannot transform value {!r}".format(value)) + + def __call__(self, value): + return self.on_value(value) + + +class StatementTransformer: + def on_value(self, value): + return value + + def on_Assign(self, stmt): + return Assign(self.on_value(stmt.lhs), self.on_value(stmt.rhs)) + + def on_Switch(self, stmt): + cases = OrderedDict((k, self.on_value(v)) for k, v in stmt.cases.items()) + return Switch(self.on_value(stmt.test), cases) + + def on_statements(self, stmt): + return list(flatten(self.on_statement(stmt) for stmt in self.on_statement(stmt))) + + def on_statement(self, stmt): + if isinstance(stmt, Assign): + return self.on_Assign(stmt) + elif isinstance(stmt, Switch): + return self.on_Switch(stmt) + elif isinstance(stmt, (list, tuple)): + return self.on_statements(stmt) + else: + raise TypeError("Cannot transform statement {!r}".format(stmt)) + + def __call__(self, value): + return self.on_statement(value) + + +class _ControlInserter: + def __init__(self, controls): + if isinstance(controls, Value): + controls = {"sys": controls} + self.controls = OrderedDict(controls) + + def __call__(self, fragment): + new_fragment = Fragment() + for subfragment, name in fragment.subfragments: + new_fragment.add_subfragment(self(subfragment), name) + new_fragment.add_statements(fragment.statements) + for cd_name, signals in fragment.iter_domains(): + for signal in signals: + new_fragment.drive(signal, cd_name) + if cd_name is None or cd_name not in self.controls: + continue + self._wrap_control(new_fragment, cd_name, signals) + return new_fragment + + def _wrap_control(self, fragment, cd_name, signals): + raise NotImplementedError + + +class ResetInserter(_ControlInserter): + def _wrap_control(self, fragment, cd_name, signals): + stmts = [s.eq(Const(s.reset, s.nbits)) for s in signals] + fragment.add_statements(Switch(self.controls[cd_name], {1: stmts})) + + +class CEInserter(_ControlInserter): + def _wrap_control(self, fragment, cd_name, signals): + stmts = [s.eq(s) for s in signals] + fragment.add_statements(Switch(self.controls[cd_name], {0: stmts})) diff --git a/nmigen/tools.py b/nmigen/tools.py new file mode 100644 index 0000000..8f8e7c1 --- /dev/null +++ b/nmigen/tools.py @@ -0,0 +1,22 @@ +from collections import Iterable + + +__all__ = ["flatten", "union"] + + +def flatten(i): + for e in i: + if isinstance(e, Iterable): + yield from flatten(e) + else: + yield e + + +def union(i): + r = None + for e in i: + if r is None: + r = e + else: + r |= e + return r diff --git a/nmigen/tracer.py b/nmigen/tracer.py new file mode 100644 index 0000000..e3f9f93 --- /dev/null +++ b/nmigen/tracer.py @@ -0,0 +1,36 @@ +import inspect +from opcode import opname + + +class NameNotFound(Exception): + pass + + +def get_var_name(depth=2): + frame = inspect.currentframe() + for _ in range(depth): + frame = frame.f_back + + code = frame.f_code + call_index = frame.f_lasti + call_opc = opname[code.co_code[call_index]] + if call_opc != "CALL_FUNCTION" and call_opc != "CALL_FUNCTION_KW": + return None + + index = call_index + 2 + while True: + opc = opname[code.co_code[index]] + if opc in ("STORE_NAME", "STORE_ATTR"): + name_index = int(code.co_code[index + 1]) + return code.co_names[name_index] + elif opc == "STORE_FAST": + name_index = int(code.co_code[index + 1]) + return code.co_varnames[name_index] + elif opc == "STORE_DEREF": + name_index = int(code.co_code[index + 1]) + return code.co_cellvars[name_index] + elif opc in ("LOAD_GLOBAL", "LOAD_ATTR", "LOAD_FAST", "LOAD_DEREF", + "DUP_TOP", "BUILD_LIST"): + index += 2 + else: + raise NameNotFound diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2ab0a80 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +import os +from os import path + +from setuptools import setup, find_packages + + +setup( + name="nmigen", + version="0.1", + author="whitequark", + author_email="whitequark@whitequark.org", + description="Python toolbox for building complex digital hardware", + #long_description="""TODO""", + license="BSD", + packages=find_packages(), +)