5 Commits
Author SHA1 Message Date
purpl3F0xandClaude Opus 5.5 6b861553b7 Pack and center Verilog block ports with automatic spacing
- Add `ports-align` ("center" by default, "top", "bottom", or "spread" for
  the previous evenly spread layout) and `ports-pitch` to `verilog.block`.
- Packed layouts use minimal margins, only reserving room for the module
  name and north/south ports, and size the block to the busiest side.
- The automatic height now also adapts to custom `ports-margins`.
- Regenerate the gallery image and the manual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-11-03 13:31:07 +02:00
purpl3F0x 7c3eb26aa3 Enhance Verilog support by adding BRAM read/write port handling and updating documentation
Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-11-03 13:18:43 +02:00
purpl3F0x 58f821070a Add APB support
Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-11-03 13:03:22 +02:00
purpl3F0x e60bfd4b43 Enhance Verilog support by adding Vivado BRAM interface handling and updating documentation
Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-10-25 11:28:17 +02:00
purpl3F0x 0c1272e35e Add Verilog parsing and rendering support
- Introduced `verilog.typ` for parsing Verilog/SystemVerilog source code, supporting both ANSI and non-ANSI port styles.
- Implemented functions to analyze modules, infer AXI4, AXI4-Lite, and AXI4-Stream buses, as well as clock and reset signals.
- Added rendering capabilities for Verilog modules, allowing for visual representation of ports and buses.
- Created a new `gallery/verilog.typ` file to facilitate the drawing of Verilog blocks in diagrams.

Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
2026-09-10 12:17:54 +01:00
11 changed files with 1014 additions and 2 deletions

No files matched your search

+26
View File
@@ -44,6 +44,16 @@ Circuiteria is a [Typst](https://typst.app) package for drawing block circuit di
<td>Groups</td>
<td>Rotated</td>
</tr>
<tr>
<td colspan="2">
<a href="./gallery/verilog.typ">
<img src="./gallery/verilog.png" width="500px" alt="Block automatically generated from a Verilog module">
</a>
</td>
</tr>
<tr>
<td colspan="2">Generated from a Verilog module</td>
</tr>
</table>
> **Note**\
@@ -62,3 +72,19 @@ To use this package, simply import [circuiteria](https://typst.app/universe/pack
...
})
```
### Verilog modules
Blocks can be generated automatically from Verilog/SystemVerilog sources.
AXI4, AXI4-Lite, AXI4-Stream, APB and BRAM signals (Vivado BRAM interface, or
`<prefix>_addr`/`_dout`/`_wr_en`/`_wr_addr`/`_wr_data`/`_rd_addr`/`_rd_data`
RAM ports) 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"`). Unprefixed buses (e.g. bare `PADDR`, `PSEL`,
...) are named after their kind (`"my_module-port-apb"`).
+76
View File
@@ -0,0 +1,76 @@
`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,
// APB master
output wire [ADDR_WIDTH-1:0] m_apb_paddr,
output wire m_apb_psel,
output wire m_apb_penable,
output wire m_apb_pwrite,
output wire [31:0] m_apb_pwdata,
input wire [31:0] m_apb_prdata,
input wire m_apb_pready,
input wire m_apb_pslverr,
// Vivado BRAM controller interface
(* X_INTERFACE_INFO = "xilinx.com:interface:bram:1.0 tile_ram CLK",
X_INTERFACE_PARAMETER = "MASTER_TYPE BRAM_CTRL, MEM_ECC NONE, MEM_WIDTH 128,
MEM_SIZE 65536, READ_WRITE_MODE READ_ONLY, READ_LATENCY 3" *)
output wire tile_ram_clk,
(* X_INTERFACE_INFO = "xilinx.com:interface:bram:1.0 tile_ram RST",
X_INTERFACE_PARAMETER = "POLARITY ACTIVE_HIGH" *)
output wire tile_ram_rst,
(* X_INTERFACE_INFO = "xilinx.com:interface:bram:1.0 tile_ram EN" *)
output wire tile_ram_en,
(* X_INTERFACE_INFO = "xilinx.com:interface:bram:1.0 tile_ram ADDR" *)
output wire [15:0] tile_ram_addr,
(* X_INTERFACE_INFO = "xilinx.com:interface:bram:1.0 tile_ram DOUT" *)
input wire [127:0] tile_ram_dout,
input wire enable,
output wire [3:0] status,
output wire irq
);
endmodule
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+12
View File
@@ -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
)
})
BIN
View File
Binary file not shown.
+19
View File
@@ -216,3 +216,22 @@ If you have installed Circuiteria directly in your project, import #link("src/li
)
#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)
+1
View File
@@ -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 (`<start>`, `<end>`) margins (numbers)
/// - fill (none, color): Fill color
+15
View File
@@ -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},
+1
View File
@@ -5,4 +5,5 @@
#import "element.typ"
#import "gates.typ"
#import "util.typ"
#import "verilog.typ"
#import "wire.typ"
+860
View File
@@ -0,0 +1,860 @@
#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 attr-depth = 0
let in-string = false
let cur = ()
let chars = text.clusters()
let i = 0
while i < chars.len() {
let c = chars.at(i)
let next = if i + 1 < chars.len() { chars.at(i + 1) } else { none }
if c == "\"" {
in-string = not in-string
}
if not in-string and c == "(" and next == "*" {
attr-depth += 1
} else if not in-string and c == "*" and next == ")" and attr-depth > 0 {
attr-depth -= 1
} else if not in-string and attr-depth == 0 and c in ("(", "[", "{") {
depth += 1
} else if not in-string and attr-depth == 0 and c in (")", "]", "}") {
depth -= 1
}
if c == "," and depth <= 0 and attr-depth == 0 and not in-string {
items.push(cur.join())
cur = ()
} else {
cur.push(c)
}
i += 1
}
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 _parse-attributes(text) = {
let attrs = (:)
for m in text.matches(regex("(?s)\\(\\*\\s*(.*?)\\s*\\*\\)")) {
let body = m.captures.at(0)
let info = body.match(regex("X_INTERFACE_INFO\\s*=\\s*\"([^\"]+)\""))
if info != none {
let value = info.captures.at(0)
attrs.insert("x-interface-info", value)
let parsed = value.match(regex("(?i)^[^\\s:]+:interface:([^:\\s]+):([^\\s]+)\\s+(\\S+)\\s+(\\S+)"))
if parsed != none {
attrs.insert("interface", (
kind: lower(parsed.captures.at(0)),
version: parsed.captures.at(1),
name: parsed.captures.at(2),
signal: lower(parsed.captures.at(3))
))
}
}
let params = body.match(regex("X_INTERFACE_PARAMETER\\s*=\\s*\"([^\"]+)\""))
if params != none {
attrs.insert("x-interface-parameter", params.captures.at(0))
}
}
return attrs
}
#let _strip-attributes(text) = text.replace(regex("(?s)\\(\\*.*?\\*\\)"), " ")
#let _make-port(name, dir, dims, attrs: (:)) = {
let range = _first-range(dims)
return (
name: name,
dir: dir,
range: range,
width: _range-width(range),
attrs: attrs
)
}
// ---------------------------------------------------------------------------
// Parsing
// ---------------------------------------------------------------------------
#let _parse-ansi-ports(portlist) = {
let ports = ()
let last-dir = "input"
let last-dims = none
for item in _split-commas(portlist) {
let attrs = _parse-attributes(item)
let clean = _strip-attributes(item)
let m = clean.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, attrs: attrs))
}
return ports
}
#let _parse-body-ports(portlist, body) = {
let decls = (:)
for m in body.matches(regex(
"(?s)((?:\\(\\*.*?\\*\\)\\s*)*)\\b(input|output|inout)\\b((?:\\s+" + _net-kw + ")*)\\s*" +
"((?:\\[[^\\]]*\\]\\s*)*)([^;]*);"
)) {
let attrs = _parse-attributes(m.captures.at(0))
let dir = m.captures.at(1)
let dims = m.captures.at(3)
for n in _split-commas(m.captures.at(4)) {
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, attrs: attrs))
}
}
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"
)
// AMBA APB3/APB4/APB5 signals ("pslverror" is the SpinalHDL spelling)
#let apb-suffixes = (
"paddr", "pprot", "pnse", "psel", "penable", "pwrite", "pwdata", "pstrb",
"pready", "prdata", "pslverr", "pslverror", "pwakeup",
"pauser", "pwuser", "pruser", "pbuser"
)
#let bram-suffixes = (
"clk", "rst", "en", "we", "wen", "regce", "addr", "din", "dout",
// separate read/write ports (e.g. simple dual-port RAMs)
"rd_en", "rd_addr", "rd_data", "wr_en", "wr_addr", "wr_data"
)
// 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"
)
// `pclk`/`presetn` are the APB names (a bare `preset` is too common a word)
#let _clock-re = regex("(?i)(^|[^a-z0-9])([ap]?clk|clock)([0-9]*)($|[^a-z0-9])")
#let _reset-re = regex("(?i)(^|[^a-z0-9])(a?rst|a?reset|preset_?n)(_?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
#let _reset-active-low(port) = {
let attrs = port.at("attrs", default: (:))
let params = attrs.at("x-interface-parameter", default: "")
if params.match(regex("(?i)POLARITY\\s+ACTIVE_LOW")) != none { return true }
if params.match(regex("(?i)POLARITY\\s+ACTIVE_HIGH")) != none { return false }
return _is-active-low(port.name)
}
// 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)
}
// Suffix tables tried in order when grouping ports by name
#let _bus-suffixes = (
("axi-stream", axis-suffixes),
("axi", axi-suffixes),
("apb", apb-suffixes),
("bram", bram-suffixes)
)
// Vivado interface types (from `X_INTERFACE_INFO`) mapped to bus kinds
#let _interface-kinds = ("apb": "apb", "bram": "bram")
#let _group-signals(ports) = {
let groups = (:)
let rest = ()
for port in ports {
let kind = none
let m = none
let attrs = port.at("attrs", default: (:))
let interface = attrs.at("interface", default: none)
if interface != none and interface.kind in _interface-kinds {
kind = _interface-kinds.at(interface.kind)
m = (prefix: interface.name, suffix: interface.signal)
} else {
for (k, suffixes) in _bus-suffixes {
m = _match-suffix(port.name, suffixes)
if m != none {
kind = k
break
}
}
}
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 kind == "bram" {
// the address requirement keeps FIFOs (`wr_en`, `rd_data`, ...) out
let has-addr = ("addr", "wr_addr", "rd_addr").any(it => it in sig)
let has-data = ("din", "dout", "wr_data", "rd_data").any(it => it in sig)
if not has-addr or not has-data { return none }
} else if kind == "apb" {
if "paddr" not in sig or "penable" not 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 if kind == "bram" {
sig.at("addr", default: sig.at("wr_addr", default: sig.at("rd_addr", default: none)))
} else if kind == "apb" {
sig.paddr
} 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", "din", "dout", "wr_data", "rd_data", "pwdata", "prdata"),
addr-width: _bus-width(group, "awaddr", "araddr", "addr", "wr_addr", "rd_addr", "paddr"),
signals: sig,
ports: sig.values()
)
}
/// Analyses the ports of a module and infers AXI4, AXI4-Lite, AXI4-Stream,
/// APB and BRAM 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"`, `"axi-stream"`, `"apb"` or `"bram"`), `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 (groups, rest) = _group-signals(module.ports)
let buses = ()
let ungrouped = rest
for (_, group) in groups {
let bus = _finalize-bus(group)
if bus == none {
ungrouped += group.signals.values()
} else {
buses.push(bus)
}
}
buses = buses.sorted(key: it => it.prefix)
let clocks = ()
let resets = ()
let signals = ()
for port in ungrouped {
if _is-clock(port.name) {
clocks.push(port)
} else if _is-reset(port.name) {
resets.push(port + (active-low: _reset-active-low(port)))
} else {
signals.push(port)
}
}
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",
"apb": "APB",
"bram": "BRAM"
)
#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((
// unprefixed buses (e.g. bare `PADDR`, `PSEL`, ...) are named by their kind
id: if bus.prefix != "" { bus.prefix } else { bus.kind },
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, AXI4-Stream, APB and BRAM 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, or the bus kind
/// such as `"apb"` for unprefixed buses), so wires can be attached with
/// `"<id>-port-<signal>"`.
///
/// - 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 and the west/east margins 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-align (str): Layout of the west/east ports: `"top"`, `"center"` or `"bottom"` pack them `ports-pitch` apart with minimal margins, `"spread"` spreads them evenly over the height
/// - ports-pitch (auto, number): Spacing between two consecutive packed west/east ports. Computed from the labels if `auto`
/// - ports-margins (dictionary): Dictionary of ports margins, as (`<start>`, `<end>`) ratios per side. On the west and east sides these are the top and bottom margins: `(8%, 14%)` by default with `ports-align: "spread"`, `(0%, 0%)` otherwise (room for the name and the north/south ports is then reserved automatically). The automatic height adapts to them
/// - 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-align: "center",
ports-pitch: auto,
ports-margins: (:),
debug: (ports: false)
) = {
assert(
ports-align in ("top", "center", "bottom", "spread"),
message: "ports-align must be \"top\", \"center\", \"bottom\" or \"spread\""
)
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 packed = ports-align != "spread"
let margins = ports-margins
for side in ("west", "east") {
if side not in margins { margins.insert(side, if packed { (0%, 0%) } else { (8%, 14%) }) }
}
// Fraction of the height left to the ports of a side between its margins
let used(side) = {
let (top, bottom) = margins.at(side)
calc.max(100% - top - bottom, 10%) / 100%
}
// Room taken inside the block by the north/south ports and the name, kept
// clear of the packed west/east ports
let edge-room(side) = {
let lens = ports.at(side).map(it => if type(it.name) == str { it.name.len() } else { 0 })
let room = if lens.len() == 0 { 0 } else { calc.max(0.4, _text-width(calc.max(..lens)) + 0.2) }
if name != none and name-anchor.starts-with(side) { room = calc.max(room, 0.6) }
room
}
let top-room = edge-room("north")
let bottom-room = edge-room("south")
// Packed ports all share the same spacing, larger with two-line (bus) labels
let pitch = if ports-pitch != auto { ports-pitch } else {
let weights = ("west", "east").filter(side => ports.at(side).len() > 0)
.map(side => rows-weight(side) / ports.at(side).len())
0.9 * calc.max(1, ..weights)
}
let h = if h != auto { h } else if packed {
let side-h(side) = (ports.at(side).len() * pitch + top-room + bottom-room) / used(side)
calc.max(1.8, side-h("west"), side-h("east"))
} else {
// each row needs 1.1375 units of the height left between the margins
let side-h(side) = rows-weight(side) * 1.1375 / used(side)
calc.max(1.8, side-h("west"), side-h("east"), if vert-count > 0 { 2.2 } else { 0 })
}
if packed {
// The element spreads n ports evenly between the margins, at
// (i + 1) / (n + 1) of the way: pick the (possibly negative) margins that
// put them `pitch` apart, aligned inside the free area
let align = ("top": 0, "center": 0.5, "bottom": 1).at(ports-align)
for side in ("west", "east") {
let n = ports.at(side).len()
if n == 0 { continue }
let area-top = h * margins.at(side).at(0) / 100% + top-room
let area = h * used(side) - top-room - bottom-room
let first = area-top + align * (area - n * pitch) + pitch / 2
let start = (first - pitch) / h
let span = pitch * (n + 1) / h
margins.insert(side, (start * 100%, (1 - start - span) * 100%))
}
}
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.2
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:( thickness: wire.signal-width, dash: if k != 0 { "solid" } else { "densely-dashed"}) , )
}
} 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)
}
}
}
}
}
+4 -2
View File
@@ -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 {