diff --git a/README.md b/README.md index 2b6cb22..0731cc6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,16 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di Groups Rotated + + + + Block automatically generated from a Verilog module + + + + + Generated from a Verilog module + > **Note**\ @@ -61,4 +71,17 @@ To use this package, simply import [circuiteria](https://typst.app/universe/pack import circuiteria: * ... }) -``` \ No newline at end of file +``` + +### Verilog modules +Blocks can be generated automatically from Verilog/SystemVerilog sources. +AXI4, AXI4-Lite and AXI4-Stream signals are grouped into single bus ports, and +clock/reset signals are detected and placed together: +```typ +#circuiteria.circuit({ + import circuiteria: * + verilog.block(read("my_module.v"), x: 0, y: 0) +}) +``` +The port ids are the signal names, or the bus prefixes for inferred buses +(e.g. `"my_module-port-s_axi"`). \ No newline at end of file diff --git a/gallery/axi_demo.v b/gallery/axi_demo.v new file mode 100644 index 0000000..31fd7bb --- /dev/null +++ b/gallery/axi_demo.v @@ -0,0 +1,51 @@ +`timescale 1ns / 1ps + +// Simple AXI-Stream to AXI4-Lite bridge used as a demo for the Verilog parser +module axi_demo #( + parameter integer DATA_WIDTH = 32, + parameter integer ADDR_WIDTH = 12 +) ( + input wire aclk, + input wire aresetn, + + // AXI4-Lite slave interface + input wire [ADDR_WIDTH-1:0] s_axil_awaddr, + input wire [2:0] s_axil_awprot, + input wire s_axil_awvalid, + output wire s_axil_awready, + input wire [31:0] s_axil_wdata, + input wire [3:0] s_axil_wstrb, + input wire s_axil_wvalid, + output wire s_axil_wready, + output wire [1:0] s_axil_bresp, + output wire s_axil_bvalid, + input wire s_axil_bready, + input wire [ADDR_WIDTH-1:0] s_axil_araddr, + input wire [2:0] s_axil_arprot, + input wire s_axil_arvalid, + output wire s_axil_arready, + output wire [31:0] s_axil_rdata, + output wire [1:0] s_axil_rresp, + output wire s_axil_rvalid, + input wire s_axil_rready, + + // AXI4-Stream sink + input wire [63:0] s_axis_tdata, + input wire [7:0] s_axis_tkeep, + input wire s_axis_tvalid, + output wire s_axis_tready, + input wire s_axis_tlast, + + // AXI4-Stream source + output wire [63:0] m_axis_tdata, + output wire [7:0] m_axis_tkeep, + output wire m_axis_tvalid, + input wire m_axis_tready, + output wire m_axis_tlast, + + input wire enable, + output wire [3:0] status, + output wire irq +); + +endmodule diff --git a/gallery/verilog.png b/gallery/verilog.png new file mode 100644 index 0000000..1afa240 Binary files /dev/null and b/gallery/verilog.png differ diff --git a/gallery/verilog.typ b/gallery/verilog.typ new file mode 100644 index 0000000..28ad010 --- /dev/null +++ b/gallery/verilog.typ @@ -0,0 +1,12 @@ +#import "/src/lib.typ" as circuiteria: circuit, util, verilog + +#set page(width: auto, height: auto, margin: .5cm) + +#circuit({ + verilog.block( + read("axi_demo.v"), + x: 0, y: 0, + // fill: util.colors.yellow, + // radius: 0.05em + ) +}) diff --git a/manual.pdf b/manual.pdf index f4e1052..e904b2a 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index ebe0c88..4fb08ae 100644 --- a/manual.typ +++ b/manual.typ @@ -215,4 +215,23 @@ If you have installed Circuiteria directly in your project, import #link("src/li ) ) -#tidy.show-module(gates-docs, sort-functions: false) \ No newline at end of file +#tidy.show-module(gates-docs, sort-functions: false) + +#pagebreak() + +#let verilog-docs = tidy.parse-module( + read("src/verilog.typ"), + name: "verilog", + old-syntax: true, + scope: ( + element: element, + circuit: circuit, + draw: draw, + wire: wire, + tidy: tidy, + examples: examples, + doc-ref: doc-ref + ) +) + +#tidy.show-module(verilog-docs, sort-functions: false) \ No newline at end of file diff --git a/src/elements/element.typ b/src/elements/element.typ index 09adfd3..acc9a06 100644 --- a/src/elements/element.typ +++ b/src/elements/element.typ @@ -33,6 +33,7 @@ /// - `id` (`str`): (Required) Port id /// - `name` (`str`): Optional name displayed *in* the block /// - `clock` (`bool`): Whether it is a clock port (triangle symbol) +/// - `invert` (`bool`): Whether it is an active-low port (bubble symbol) /// - `vertical` (`bool`): Whether the name should be drawn vertically /// - ports-margins (dictionary): Dictionary of ports margins (used with automatic port placement). They keys are cardinal directions ("north", "east", "south", "west"). The values are tuples of (``, ``) margins (numbers) /// - fill (none, color): Fill color diff --git a/src/elements/ports.typ b/src/elements/ports.typ index b24cdfc..8c29e52 100644 --- a/src/elements/ports.typ +++ b/src/elements/ports.typ @@ -27,6 +27,21 @@ // TODO: use context or vectors to have the height relative to the width draw.line(prev, pos1, next) } + if (port.at("invert", default: false)) { + let r = 0.1 + let offset + if (side == "north") { offset = ( 0, r) } + else if (side == "east") { offset = ( r, 0) } + else if (side == "south") { offset = ( 0, -r) } + else if (side == "west") { offset = ( -r, 0) } + + draw.circle( + (rel: offset, to: pos), + radius: r, + fill: white, + stroke: black + 1pt + ) + } draw.content( pos, anchor: if name-rotate {rotate-anchor(side)} else {side}, diff --git a/src/lib.typ b/src/lib.typ index 3970ead..49f35d2 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -5,4 +5,5 @@ #import "element.typ" #import "gates.typ" #import "util.typ" +#import "verilog.typ" #import "wire.typ" \ No newline at end of file diff --git a/src/verilog.typ b/src/verilog.typ new file mode 100644 index 0000000..a66b469 --- /dev/null +++ b/src/verilog.typ @@ -0,0 +1,704 @@ +#import "/src/cetz.typ": draw +#import "elements/block.typ": block as block-element +#import "util.typ" +#import "wire.typ" + +// --------------------------------------------------------------------------- +// Low level helpers +// --------------------------------------------------------------------------- + +#let _ident = "(?:\\\\\\S+|[A-Za-z_][A-Za-z0-9_$]*)" +#let _net-kw = "(?:var|wire|reg|logic|bit|tri|tri0|tri1|triand|trior|wand|wor|uwire|supply0|supply1|signed|unsigned)" + +#let _strip-comments(src) = { + let s = src.replace(regex("(?s)/\\*.*?\\*/"), " ") + s = s.replace(regex("//[^\n]*"), " ") + return s +} + +// Finds the index of the closing bracket matching the opening one at `start` +#let _find-close(chars, start) = { + let pairs = ("(": ")", "[": "]", "{": "}") + let open = chars.at(start) + let close = pairs.at(open) + let depth = 0 + let i = start + while i < chars.len() { + let c = chars.at(i) + if c == open { + depth += 1 + } else if c == close { + depth -= 1 + if depth == 0 { return i } + } + i += 1 + } + return none +} + +// Splits a text on commas that are not nested inside brackets +#let _split-commas(text) = { + let items = () + let depth = 0 + let cur = () + for c in text.clusters() { + if c in ("(", "[", "{") { + depth += 1 + } else if c in (")", "]", "}") { + depth -= 1 + } + if c == "," and depth <= 0 { + items.push(cur.join()) + cur = () + } else { + cur.push(c) + } + } + if cur.join().trim() != "" { items.push(cur.join()) } + return items.filter(it => it.trim() != "") +} + +// Evaluates a very simple integer expression (`4`, `32-1`, `8+2`) +#let _eval-int(expr) = { + if expr == none { return none } + let e = expr.replace(regex("\\s+"), "") + if e.match(regex("^[0-9]+$")) != none { return int(e) } + let m = e.match(regex("^([0-9]+)([+*-])([0-9]+)$")) + if m == none { return none } + let a = int(m.captures.at(0)) + let b = int(m.captures.at(2)) + let op = m.captures.at(1) + if op == "+" { return a + b } + if op == "-" { return a - b } + return a * b +} + +// Extracts the first packed range (`[7:0]`) of a dimension list +#let _first-range(dims) = { + if dims == none { return none } + let m = dims.match(regex("\\[([^\\]]*)\\]")) + if m == none { return none } + return m.captures.at(0).trim() +} + +// Computes the width of a range text (`7:0` -> 8), `none` if not constant +#let _range-width(range) = { + if range == none { return 1 } + let m = range.match(regex("^([^:]+):([^:]+)$")) + if m == none { return none } + let hi = _eval-int(m.captures.at(0)) + let lo = _eval-int(m.captures.at(1)) + if hi == none or lo == none { return none } + return calc.abs(hi - lo) + 1 +} + +#let _make-port(name, dir, dims) = { + let range = _first-range(dims) + return ( + name: name, + dir: dir, + range: range, + width: _range-width(range) + ) +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +#let _parse-ansi-ports(portlist) = { + let ports = () + let last-dir = "input" + let last-dims = none + for item in _split-commas(portlist) { + let m = item.match(regex( + "^\\s*(?:(input|output|inout)\\s+)?(?:" + _net-kw + "\\s+)*" + + "((?:\\[[^\\]]*\\]\\s*)*)(" + _ident + ")" + )) + if m == none { continue } + let (dir, dims, name) = (m.captures.at(0), m.captures.at(1), m.captures.at(2)) + if dir != none { + last-dir = dir + last-dims = dims + } else if dims == none or dims.trim() == "" { + dims = last-dims + } + ports.push(_make-port(name, last-dir, dims)) + } + return ports +} + +#let _parse-body-ports(portlist, body) = { + let decls = (:) + for m in body.matches(regex( + "(?s)\\b(input|output|inout)\\b((?:\\s+" + _net-kw + ")*)\\s*" + + "((?:\\[[^\\]]*\\]\\s*)*)([^;]*);" + )) { + let dir = m.captures.at(0) + let dims = m.captures.at(2) + for n in _split-commas(m.captures.at(3)) { + let nm = n.match(regex("^\\s*(" + _ident + ")\\s*$")) + if nm == none { continue } + decls.insert(nm.captures.at(0), _make-port(nm.captures.at(0), dir, dims)) + } + } + + let ports = () + for item in _split-commas(portlist) { + let m = item.match(regex("(" + _ident + ")\\s*$")) + if m == none { continue } + let name = m.captures.at(0) + if name in decls { ports.push(decls.at(name)) } + } + // Declared but not listed (or unparsable port list): append the rest + for (name, port) in decls { + if ports.find(it => it.name == name) == none { ports.push(port) } + } + return ports +} + +#let _parse-module(chunk) = { + let m = chunk.match(regex("^\\s*module\\s+(" + _ident + ")")) + if m == none { return none } + let name = m.captures.at(0) + let chars = chunk.slice(m.end).clusters() + + let i = 0 + while i < chars.len() and chars.at(i).trim() == "" { i += 1 } + + let params = () + if i < chars.len() and chars.at(i) == "#" { + while i < chars.len() and chars.at(i) != "(" { i += 1 } + let close = _find-close(chars, i) + if close == none { return none } + for p in _split-commas(chars.slice(i + 1, close).join()) { + let pm = p.match(regex("(" + _ident + ")\\s*=\\s*([^,]+)$")) + if pm != none { + params.push((name: pm.captures.at(0), value: pm.captures.at(1).trim())) + } + } + i = close + 1 + } + + while i < chars.len() and chars.at(i) != "(" and chars.at(i) != ";" { i += 1 } + + let portlist = "" + if i < chars.len() and chars.at(i) == "(" { + let close = _find-close(chars, i) + if close == none { return none } + portlist = chars.slice(i + 1, close).join() + i = close + 1 + } + let body = if i < chars.len() { chars.slice(i).join() } else { "" } + + let ports = if portlist.match(regex("\\b(input|output|inout)\\b")) != none { + _parse-ansi-ports(portlist) + } else { + _parse-body-ports(portlist, body) + } + + return (name: name, params: params, ports: ports) +} + +/// Parses Verilog/SystemVerilog source code and returns the declared modules. +/// +/// Both ANSI (`module m(input clk, ...)`) and non-ANSI +/// (`module m(clk, ...); input clk;`) port styles are supported. +/// +/// ```typ +/// #let modules = verilog.parse(read("my_module.v")) +/// ``` +/// +/// - source (str): The Verilog source code, typically obtained with `read()` +/// -> array +#let parse(source) = { + let src = _strip-comments(source) + let modules = () + for m in src.matches(regex("\\bmodule\\b")) { + let rest = src.slice(m.start) + let pos = rest.slice(6).position(regex("\\bendmodule\\b")) + let chunk = if pos == none { rest } else { rest.slice(0, 6 + pos) } + let parsed = _parse-module(chunk) + if parsed != none { modules.push(parsed) } + } + return modules +} + +/// Returns a single module from Verilog source code. +/// +/// - source (str, array, dictionary): Verilog source code, an array of parsed modules or an already parsed module +/// - name (none, str): Name of the wanted module. If `none`, the first module is returned +/// -> dictionary +#let get-module(source, name: none) = { + let modules = if type(source) == str { + parse(source) + } else if type(source) == dictionary { + (source,) + } else { + source + } + if modules.len() == 0 { panic("No Verilog module found") } + if name == none { return modules.first() } + let found = modules.find(it => it.name == name) + if found == none { panic("Could not find Verilog module " + name) } + return found +} + +// --------------------------------------------------------------------------- +// Bus / clock / reset inference +// --------------------------------------------------------------------------- + +#let axis-suffixes = ( + "tvalid", "tready", "tdata", "tlast", "tkeep", "tstrb", + "tuser", "tid", "tdest", "twakeup" +) + +#let axi-suffixes = ( + "awid", "awaddr", "awlen", "awsize", "awburst", "awlock", "awcache", + "awprot", "awqos", "awregion", "awuser", "awvalid", "awready", + "wid", "wdata", "wstrb", "wlast", "wuser", "wvalid", "wready", + "bid", "bresp", "buser", "bvalid", "bready", + "arid", "araddr", "arlen", "arsize", "arburst", "arlock", "arcache", + "arprot", "arqos", "arregion", "aruser", "arvalid", "arready", + "rid", "rdata", "rresp", "rlast", "ruser", "rvalid", "rready" +) + +// Signals that only exist on full AXI4 (used to tell AXI4 from AXI4-Lite) +#let axi-full-only = ( + "awid", "awlen", "awsize", "awburst", "awlock", "awcache", "awqos", + "awregion", "wid", "wlast", "bid", + "arid", "arlen", "arsize", "arburst", "arlock", "arcache", "arqos", + "arregion", "rid", "rlast" +) + +#let _clock-re = regex("(?i)(^|[^a-z0-9])(a?clk|clock)([0-9]*)($|[^a-z0-9])") +#let _reset-re = regex("(?i)(^|[^a-z0-9])(a?rst|a?reset)(_?n)?([0-9]*)($|[^a-z0-9])") + +#let _is-clock(name) = name.match(_clock-re) != none +#let _is-reset(name) = name.match(_reset-re) != none +#let _is-active-low(name) = name.match(regex("(?i)(_n|_b|_l|resetn|rstn)$")) != none + +// Splits a signal name into (prefix, suffix) using the given suffix table +#let _match-suffix(name, suffixes) = { + let lower = lower(name) + let best = none + for s in suffixes { + if not lower.ends-with(s) { continue } + if lower.len() == s.len() { + if best == none or s.len() > best.len() { best = s } + continue + } + let sep = lower.at(lower.len() - s.len() - 1) + if sep != "_" { continue } + if best == none or s.len() > best.len() { best = s } + } + if best == none { return none } + let prefix = name.slice(0, name.len() - best.len()) + return (prefix: prefix.trim("_", at: end), suffix: best) +} + +#let _group-signals(ports) = { + let groups = (:) + let rest = () + for port in ports { + let kind = none + let m = _match-suffix(port.name, axis-suffixes) + if m != none { + kind = "axi-stream" + } else { + m = _match-suffix(port.name, axi-suffixes) + if m != none { kind = "axi" } + } + if m == none { + rest.push(port) + continue + } + let key = kind + "|" + lower(m.prefix) + let group = groups.at(key, default: ( + kind: kind, + prefix: m.prefix, + signals: (:) + )) + group.signals.insert(m.suffix, port) + groups.insert(key, group) + } + return (groups: groups, rest: rest) +} + +#let _bus-width(group, ..names) = { + for n in names.pos() { + if n in group.signals { + return group.signals.at(n).width + } + } + return none +} + +#let _finalize-bus(group) = { + let sig = group.signals + let kind = group.kind + + if kind == "axi-stream" { + if "tvalid" not in sig and not ("tdata" in sig and "tready" in sig) { + return none + } + } else { + if sig.len() < 4 { return none } + if "awvalid" not in sig and "arvalid" not in sig { return none } + let lite = sig.keys().find(it => it in axi-full-only) == none + if lite or lower(group.prefix).contains(regex("axil|lite")) { + kind = "axi-lite" + } + } + + let ref = if kind == "axi-stream" { + sig.at("tvalid", default: sig.at("tdata", default: none)) + } else { + sig.at("awvalid", default: sig.at("arvalid", default: none)) + } + let role = if ref != none and ref.dir == "output" { "master" } else { "slave" } + + return ( + kind: kind, + prefix: group.prefix, + role: role, + dir: if role == "master" { "output" } else { "input" }, + data-width: _bus-width(group, "tdata", "wdata", "rdata"), + addr-width: _bus-width(group, "awaddr", "araddr"), + signals: sig, + ports: sig.values() + ) +} + +/// Analyses the ports of a module and infers AXI4, AXI4-Lite and AXI4-Stream +/// buses as well as clock and reset signals. +/// +/// The returned dictionary contains: +/// - `buses`: array of inferred buses. Each bus has the fields `kind` +/// (`"axi"`, `"axi-lite"` or `"axi-stream"`), `prefix`, `role` +/// (`"master"` or `"slave"`), `dir`, `data-width`, `addr-width` and `signals` +/// - `clocks`: array of clock ports +/// - `resets`: array of reset ports (with an additional `active-low` field) +/// - `signals`: array of the remaining ports +/// +/// - module (str, array, dictionary): Verilog source code or a parsed module +/// - name (none, str): Name of the wanted module (when several are present) +/// -> dictionary +#let analyze(module, name: none) = { + let module = get-module(module, name: name) + + let clocks = () + let resets = () + let others = () + for port in module.ports { + if _is-clock(port.name) { + clocks.push(port) + } else if _is-reset(port.name) { + resets.push(port + (active-low: _is-active-low(port.name))) + } else { + others.push(port) + } + } + + let (groups, rest) = _group-signals(others) + let buses = () + let signals = rest + for (_, group) in groups { + let bus = _finalize-bus(group) + if bus == none { + signals += group.signals.values() + } else { + buses.push(bus) + } + } + buses = buses.sorted(key: it => it.prefix) + + return ( + name: module.name, + params: module.params, + buses: buses, + clocks: clocks, + resets: resets, + signals: signals, + ports: module.ports + ) +} + +// --------------------------------------------------------------------------- +// Rendering +// --------------------------------------------------------------------------- + +#let bus-kind-names = ( + "axi": "AXI4", + "axi-lite": "AXI4-Lite", + "axi-stream": "AXI4-Stream" +) + +#let _bus-label(bus, show-widths: true, show-info: false) = { + let title = if bus.prefix != "" { bus.prefix } else { bus-kind-names.at(bus.kind) } + let info = () + if show-info { + if bus.prefix != "" { info.push(bus-kind-names.at(bus.kind)) } + if show-widths and bus.data-width != none { + info.push(str(bus.data-width) + "b") + } + } + if info.len() == 0 { + return (label: title, len: title.len()) + } + let sub = info.join(" ") + return ( + label: box(align(left)[#title \ #text(size: 0.7em, sub)]), + len: calc.max(title.len(), int(sub.len() * 0.7)) + ) +} + +#let _signal-label(port, show-widths: true) = { + let label = if show-widths and port.range != none { + port.name + "[" + port.range + "]" + } else { + port.name + } + return (label: label, len: label.len()) +} + +// Approximate width (in canvas units) of a label of `len` characters +#let _text-width(len) = 0.28 * len + +/// Builds the ports dictionary of a Verilog module, ready to be given to +/// #doc-ref("element.elmt"). +/// +/// Inputs (and slave buses) are placed on the west side, outputs (and master +/// buses) on the east side, clocks and resets on top of the west side. +/// +/// - module (str, array, dictionary): Verilog source code or a parsed module +/// - name (none, str): Name of the wanted module (when several are present) +/// - show-widths (bool): Whether to append the signal widths to their names +/// - show-bus-info (bool): Whether to display the bus kind and data width below the bus name +/// - clock-side (str): Side on which clock ports are placed +/// - reset-side (str): Side on which reset ports are placed +/// -> dictionary +#let module-ports( + module, + name: none, + show-widths: true, + show-bus-info: false, + clock-side: "west", + reset-side: "west" +) = { + let info = if type(module) == dictionary and "buses" in module { + module + } else { + analyze(module, name: name) + } + + let ports = (north: (), east: (), south: (), west: ()) + + for bus in info.buses { + let side = if bus.dir == "output" { "east" } else { "west" } + let label = _bus-label(bus, show-widths: show-widths, show-info: show-bus-info) + ports.at(side).push(( + id: bus.prefix, + name: label.label, + label-len: label.len, + bus: true, + kind: bus.kind, + multiline: type(label.label) != str + )) + } + + for port in info.clocks { + let vertical = clock-side in ("north", "south") + ports.at(clock-side).push(( + id: port.name, + // indented so that the label does not collide with the clock triangle + name: if vertical { "" } else { box(inset: (left: 0.7em), port.name) }, + label-len: port.name.len() + 2, + clock: true, + small: true + )) + } + + for port in info.resets { + ports.at(reset-side).push(( + id: port.name, + name: port.name, + label-len: port.name.len(), + invert: port.active-low, + vertical: reset-side in ("north", "south") + )) + } + + for port in info.signals { + let side = if port.dir == "output" { "east" } else { "west" } + let label = _signal-label(port, show-widths: show-widths) + ports.at(side).push(( + id: port.name, + name: label.label, + label-len: label.len, + invert: _is-active-low(port.name), + bus: port.width == none or port.width > 1 + )) + } + + return ports +} + +/// Draws a block from a Verilog module description. +/// +/// AXI4, AXI4-Lite and AXI4-Stream signals are grouped into single bus ports, +/// and clock/reset signals are automatically detected and grouped together. +/// +/// ```typ +/// #circuiteria.circuit({ +/// circuiteria.verilog.block(read("axi_dma.v"), x: 0, y: 0) +/// }) +/// ``` +/// +/// The port ids are the signal names (or the bus prefixes), so wires can be +/// attached with `"-port-"`. +/// +/// - module (str, array, dictionary): Verilog source code or a parsed module +/// - module-name (none, str): Name of the wanted module (when several are present) +/// - x (number, dictionary): The x position (bottom-left corner) +/// - y (number, dictionary): The y position (bottom-left corner) +/// - w (auto, number): Width of the block. Computed from the port names if `auto` +/// - h (auto, number): Height of the block. Computed from the port count if `auto` +/// - id (auto, str): The block id. Defaults to the module name +/// - name (auto, none, str, content): Name displayed in the block. Defaults to the module name +/// - name-anchor (str): Anchor for the displayed name +/// - show-widths (bool): Whether to append the signal widths to their names +/// - show-bus-info (bool): Whether to display the bus kind and data width below the bus name +/// - stubs (bool): Whether to draw an unnamed wire stub on every port +/// - stub-length (number, length): Length of the automatic stubs +/// - bus-markers (bool): Whether to draw the bus stubs as double lines +/// - clock-side (str): Side on which clock ports are placed +/// - reset-side (str): Side on which reset ports are placed +/// - fill (none, color): Fill color +/// - stroke (stroke): Border stroke +/// - radius (number, length, ratio, dictionary): The corner radius of the block +/// - ports-margins (dictionary): Dictionary of ports margins +/// - debug (dictionary): Dictionary of debug options +#let block( + module, + module-name: none, + x: 0, + y: 0, + w: auto, + h: auto, + id: auto, + name: auto, + name-anchor: "south", + show-widths: true, + show-bus-info: false, + stubs: true, + stub-length: 1em, + bus-markers: true, + clock-side: "west", + reset-side: "west", + fill: none, + stroke: black + 1pt, + radius: 0.5em, + ports-margins: (:), + debug: (ports: false) +) = { + let info = if type(module) == dictionary and "buses" in module { + module + } else { + analyze(module, name: module-name) + } + let ports = module-ports( + info, + show-widths: show-widths, + show-bus-info: show-bus-info, + clock-side: clock-side, + reset-side: reset-side + ) + + let id = if id == auto { info.name } else { id } + let name = if name == auto { raw(info.name) } else { name } + + let max-len(side) = { + let lens = ports.at(side).map(it => it.at("label-len", default: 8)) + if lens.len() == 0 { 0 } else { calc.max(..lens) } + } + let sum-len(side) = { + ports.at(side).fold(0, (acc, it) => acc + it.at("label-len", default: 8)) + } + // Two-line (bus) labels need more vertical room + let rows-weight(side) = { + ports.at(side).fold(0, (acc, it) => acc + if it.at("multiline", default: false) { 1.6 } else { 1 }) + } + + let vert-count = calc.max(ports.north.len(), ports.south.len(), 0) + + let margins = ports-margins + for side in ("west", "east") { + if side not in margins { margins.insert(side, (8%, 14%)) } + } + + let h = if h != auto { h } else { + let rows = calc.max(rows-weight("west"), rows-weight("east"), 1) + calc.max(1.8, rows * 1.05 / 0.72, if vert-count > 0 { 2.2 } else { 0 }) + } + let w = if w != auto { w } else { + let inner = _text-width(max-len("west") + max-len("east")) + 0.8 + let title = _text-width(info.name.len() * 1.2) + 0.8 + let vert-len = calc.max(sum-len("north"), sum-len("south")) + let vert = _text-width(vert-len) + 0.5 * vert-count + 0.4 + calc.max(2.5, inner, title, vert) + } + + block-element( + x: x, + y: y, + w: w, + h: h, + id: id, + name: name, + name-anchor: name-anchor, + ports: ports, + ports-margins: margins, + fill: fill, + stroke: stroke, + radius: radius, + debug: debug + ) + + if stubs { + // active-low ports carry a bubble, so their stub starts past it + let bubble = (north: (0, 0.2), east: (0.2, 0), south: (0, -0.2), west: (-0.2, 0)) + let ends = (north: (0, stub-length), east: (stub-length, 0), south: (0, -stub-length), west: (-stub-length, 0)) + let gap = 0.1 + for (side, side-ports) in ports { + for port in side-ports { + let anchor = id + "-port-" + port.id + let is-bus = port.at("bus", default: false) + if bus-markers and "kind" in port { + let perp = if side in ("east", "west") { (0, gap) } else { (gap, 0) } + // starts past the block border so the fill does not cover it + let o = 0.02 + let edge = (north: (0, o), east: (o, 0), south: (0, -o), west: (-o, 0)).at(side) + draw.rect( + (rel: (edge.at(0) - perp.at(0), edge.at(1) - perp.at(1)), to: anchor), + (rel: ends.at(side), to: (rel: perp, to: anchor)), + fill: luma(80%), + stroke: none + ) + for k in (1, 0, -1) { + let start = (rel: (perp.at(0) * k, perp.at(1) * k), to: anchor) + draw.line(start, (rel: ends.at(side), to: start), stroke: wire.signal-width) + } + } else if port.at("invert", default: false) { + draw.line( + (rel: bubble.at(side), to: anchor), + (rel: ends.at(side), to: anchor), + stroke: if is-bus { wire.bus-width } else { wire.signal-width } + ) + } else { + wire.stub(anchor, side, length: stub-length, bus: is-bus) + } + } + } + } +} diff --git a/src/wire.typ b/src/wire.typ index cbf7c4b..94d21f4 100644 --- a/src/wire.typ +++ b/src/wire.typ @@ -363,7 +363,8 @@ /// - vertical (bool): Whether the name should be displayed vertically /// - length (number): The length of the stub /// - name-offset (number): The name offset, perpendicular to the stub -#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0) = { +/// - bus (bool): Whether the stub is a bus (thicker line) +#let stub(port-id, side, name: none, vertical: false, length: 1em, name-offset: 0, bus: false) = { let end-offset = ( north: (0, length), east: (length, 0), @@ -380,7 +381,8 @@ draw.line( port-id, - (rel: end-offset, to: port-id) + (rel: end-offset, to: port-id), + stroke: if bus { bus-width } else { signal-width } ) if name != none { let text-anchor = if vertical {