Enhance Verilog support by adding Vivado BRAM interface handling and updating documentation

Signed-off-by: Stavros Avramidis <stavros9899@gmail.com>
This commit is contained in:
purpl3F0x committed 2026-10-25 11:28:17 +02:00
1 parent 0c1272e35e
commit e60bfd4b43
5 files changed
+20858 -20353

No files matched your search

+3 -2
View File
@@ -75,8 +75,9 @@ 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 and AXI4-Stream signals are grouped into single bus ports, and
clock/reset signals are detected and placed together:
AXI4, AXI4-Lite, AXI4-Stream and Vivado BRAM interface signals are grouped
into single bus ports, and clock/reset signals are detected and placed
together:
```typ
#circuiteria.circuit({
import circuiteria: *
+15
View File
@@ -43,6 +43,21 @@ module axi_demo #(
input wire m_axis_tready,
output wire m_axis_tlast,
// 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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+20722 -20310
View File
File diff suppressed because it is too large. Load diff
+118 -41
View File
@@ -40,19 +40,33 @@
#let _split-commas(text) = {
let items = ()
let depth = 0
let attr-depth = 0
let in-string = false
let cur = ()
for c in text.clusters() {
if c in ("(", "[", "{") {
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 c in (")", "]", "}") {
} else if not in-string and attr-depth == 0 and c in (")", "]", "}") {
depth -= 1
}
if c == "," and depth <= 0 {
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() != "")
@@ -92,13 +106,42 @@
return calc.abs(hi - lo) + 1
}
#let _make-port(name, dir, dims) = {
#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)
width: _range-width(range),
attrs: attrs
)
}
@@ -111,7 +154,9 @@
let last-dir = "input"
let last-dims = none
for item in _split-commas(portlist) {
let m = item.match(regex(
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 + ")"
))
@@ -123,7 +168,7 @@
} else if dims == none or dims.trim() == "" {
dims = last-dims
}
ports.push(_make-port(name, last-dir, dims))
ports.push(_make-port(name, last-dir, dims, attrs: attrs))
}
return ports
}
@@ -131,15 +176,16 @@
#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)((?:\\(\\*.*?\\*\\)\\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 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))
decls.insert(nm.captures.at(0), _make-port(nm.captures.at(0), dir, dims, attrs: attrs))
}
}
@@ -263,6 +309,10 @@
"rid", "rdata", "rresp", "rlast", "ruser", "rvalid", "rready"
)
#let bram-suffixes = (
"clk", "rst", "en", "we", "wen", "regce", "addr", "din", "dout"
)
// 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",
@@ -278,6 +328,14 @@
#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)
@@ -302,12 +360,25 @@
let rest = ()
for port in ports {
let kind = none
let m = _match-suffix(port.name, axis-suffixes)
let m = none
let attrs = port.at("attrs", default: (:))
let interface = attrs.at("interface", default: none)
if interface != none and interface.kind == "bram" {
kind = "bram"
m = (prefix: interface.name, suffix: interface.signal)
} else {
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 {
kind = "axi"
} else {
m = _match-suffix(port.name, bram-suffixes)
if m != none { kind = "bram" }
}
}
}
if m == none {
rest.push(port)
@@ -342,6 +413,8 @@
if "tvalid" not in sig and not ("tdata" in sig and "tready" in sig) {
return none
}
} else if kind == "bram" {
if "addr" not in sig or ("din" not in sig and "dout" not in sig) { return none }
} else {
if sig.len() < 4 { return none }
if "awvalid" not in sig and "arvalid" not in sig { return none }
@@ -353,6 +426,8 @@
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("din", default: sig.at("dout", default: none)))
} else {
sig.at("awvalid", default: sig.at("arvalid", default: none))
}
@@ -363,19 +438,19 @@
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"),
data-width: _bus-width(group, "tdata", "wdata", "rdata", "din", "dout"),
addr-width: _bus-width(group, "awaddr", "araddr", "addr"),
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.
/// Analyses the ports of a module and infers AXI4, AXI4-Lite, AXI4-Stream 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"` or `"axi-stream"`), `prefix`, `role`
/// (`"axi"`, `"axi-lite"`, `"axi-stream"` 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)
@@ -387,32 +462,32 @@
#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 (groups, rest) = _group-signals(module.ports)
let buses = ()
let signals = rest
let ungrouped = rest
for (_, group) in groups {
let bus = _finalize-bus(group)
if bus == none {
signals += group.signals.values()
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,
@@ -431,7 +506,8 @@
#let bus-kind-names = (
"axi": "AXI4",
"axi-lite": "AXI4-Lite",
"axi-stream": "AXI4-Stream"
"axi-stream": "AXI4-Stream",
"bram": "BRAM"
)
#let _bus-label(bus, show-widths: true, show-info: false) = {
@@ -546,8 +622,9 @@
/// 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.
/// AXI4, AXI4-Lite, AXI4-Stream and BRAM signals are grouped into single bus
/// ports, and clock/reset signals are automatically detected and grouped
/// together.
///
/// ```typ
/// #circuiteria.circuit({
@@ -669,7 +746,7 @@
// 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
let gap = 0.2
for (side, side-ports) in ports {
for port in side-ports {
let anchor = id + "-port-" + port.id
@@ -687,7 +764,7 @@
)
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)
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(