Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ The `Simulator` acts as a statically accessible driver of the overall simulation

### Synthesizer

A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog.
A separate type of object responsible for taking a `Module` and converting it to some output. ROHD includes two built-in synthesizers:

- `SystemVerilogSynthesizer` — Converts a `Module` hierarchy into SystemVerilog files. Each `Module` with custom functionality must implement the `SystemVerilog` or `InlineSystemVerilog` mixin so the synthesizer knows how to emit it.
- `NetlistSynthesizer` — Converts a `Module` hierarchy into a JSON netlist following the [Yosys JSON format](https://yosyshq.readthedocs.io/projects/yosys/en/0.45/cmd/write_json.html). The output contains `modules` with `ports`, `cells`, and `netnames` sections, and is compatible with open-source EDA tools that consume `yosys write_json` output.

Both synthesizers are driven by `SynthBuilder`, which walks the module hierarchy bottom-up, deduplicates identical module definitions, and produces a set of `SynthesisResult` objects (either `SynthesisResult` for SystemVerilog or `NetlistSynthesisResult` for netlist JSON).

## Organization

Expand All @@ -44,7 +49,7 @@ Contains a collection of `Module` implementations that can be used as primitive

### Synthesizers

Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported).
Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types. The `SystemVerilogSynthesizer` generates `.sv` files, while the `NetlistSynthesizer` generates Yosys-compatible JSON netlists. `SynthBuilder` orchestrates the synthesis process for both, handling hierarchy traversal and definition deduplication.

### Utilities

Expand Down
82 changes: 82 additions & 0 deletions doc/user_guide/_docs/A21-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,85 @@ The `Naming.unpreferredName` function will modify a signal name to indicate to d
## More advanced generation

Under the hood of `generateSynth`, it's actually using a [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) which accepts a `Module` and a `Synthesizer` (usually a `SystemVerilogSynthesizer`) as arguments. This `SynthBuilder` can provide a collection of `String` file contents via `getFileContents`, or you can ask for the full set of `synthesisResults`, which contains `SynthesisResult`s which can each be converted `toSynthFileContents` but also has context about the `module` it refers to, the `instanceTypeName`, etc. With these APIs, you can easily generate named files, add file headers, ignore generation of some modules, generate file lists for other tools, etc. The `SynthBuilder.multi` constructor makes it convenient to generate outputs for multiple independent hierarchies.

## Netlist Synthesis

In addition to SystemVerilog, ROHD can synthesize a design to a JSON netlist that follows the [Yosys JSON format](https://yosyshq.readthedocs.io/projects/yosys/en/0.45/cmd/write_json.html). This is the same format produced by `yosys write_json` and consumed by many open-source EDA tools and viewers.

### Basic usage

```dart
void main() async {
final myModule = MyModule();
await myModule.build();

final netlistJson = await NetlistSynthesizer().synthesizeToJson(myModule);

// write it to a file
File('myDesign.rohd.json').writeAsStringSync(netlistJson);
}
```

### Output format

The produced JSON has the following top-level structure:

```json
{
"creator": "ROHD ...",
"modules": {
"<definition_name>": {
"attributes": { "top": 1, ... },
"ports": {
"<port_name>": { "direction": "input"|"output"|"inout", "bits": [...] }
},
"cells": {
"<cell_name>": { "type": "...", "connections": { ... } }
},
"netnames": {
"<signal_name>": { "bits": [...], "hide_name": 0|1 }
}
}
}
}
```

Key sections per module:

- **`ports`** — The module's input, output, and inout ports. Each port has a `direction` and a `bits` array of integer wire IDs.
- **`cells`** — Sub-module instances and primitive gate cells (e.g. `$and`, `$mux`, `$dff`, `$add`). Each cell has a `type`, and in full mode, `connections` mapping port names to wire ID vectors.
- **`netnames`** — Named signals (wires) internal to the module. Each entry maps a signal name to its `bits` vector.

The top-level module is marked with `"top": 1` in its `attributes`.

### Slim mode

Passing `NetlistOptions(slimMode: true)` produces a compact JSON that omits cell `connections`. This is useful for transmitting the design dictionary (module hierarchy, ports, signals) without the full connectivity — a remote agent can then fetch connection details per module on demand.

```dart
final slimSynth = NetlistSynthesizer(
options: const NetlistOptions(slimMode: true),
);
final slimJson = await slimSynth.synthesizeToJson(myModule);
```

### Using SynthBuilder directly

Just like SystemVerilog synthesis, you can use `SynthBuilder` directly with a `NetlistSynthesizer` for more control:

```dart
final synthesizer = NetlistSynthesizer();
final synth = SynthBuilder(myModule, synthesizer);

// Access individual NetlistSynthesisResult objects
for (final result in synth.synthesisResults) {
if (result is NetlistSynthesisResult) {
print('${result.instanceTypeName}: '
'${result.ports.length} ports, '
'${result.cells.length} cells');
}
}

// Or build the combined modules map directly
final modulesMap = await synthesizer.buildModulesMap(synth, myModule);
```
40 changes: 12 additions & 28 deletions example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,16 @@
// allow `print` messages (disable lint):
// ignore_for_file: avoid_print

// Import necessary dart packages for this file.
// Import necessary dart pacakges for this file.
import 'dart:async';

// Import the ROHD package.
import 'package:rohd/rohd.dart';

// Define a class Counter that extends ROHD's abstract Module class.
class Counter extends Module {
// For convenience, map interesting outputs to short variable names for
// consumers of this module.
Logic get val => output('val');

// This counter supports any width, determined at run-time.
final int width;

Counter(Logic en, Logic reset, Logic clk,
{this.width = 8, super.name = 'counter'}) {
// Register inputs and outputs of the module in the constructor.
// Module logic must consume registered inputs and output to registered
// outputs.
en = addInput('en', en);
reset = addInput('reset', reset);
clk = addInput('clk', clk);
addOutput('val', width: width);

// We can use the `flop` function to automate creation of a `Sequential`.
val <= flop(clk, reset: reset, en: en, val + 1);
}
}
// Re-export the Counter module from the library examples so that
// existing tests that `import 'example/example.dart'` still see it.
import 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter;
export 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter;

// Let's simulate with this counter a little, generate a waveform, and take a
// look at generated SystemVerilog.
Expand Down Expand Up @@ -76,8 +57,9 @@ Future<void> main({bool noPrint = false}) async {
// Let's also print a message every time the value on the counter changes,
// just for this example to make it easier to see before we look at waves.
if (!noPrint) {
counter.val.changed
.listen((e) => print('@${Simulator.time}: Value changed: $e'));
counter.val.changed.listen(
(e) => print('@${Simulator.time}: Value changed: $e'),
);
}

// Start off with a disabled counter and asserting reset at the start.
Expand Down Expand Up @@ -115,7 +97,9 @@ Future<void> main({bool noPrint = false}) async {

// We can take a look at the waves now.
if (!noPrint) {
print('To view waves, check out waves.vcd with a waveform viewer'
' (e.g. `gtkwave waves.vcd`).');
print(
'To view waves, check out waves.vcd with a waveform viewer'
' (e.g. `gtkwave waves.vcd`).',
);
}
}
117 changes: 117 additions & 0 deletions example/filter_bank.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (C) 2025-2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// filter_bank.dart
// A polyphase FIR filter bank design example exercising:
// - Deep hierarchy with shared sub-module definitions
// - Interface (FilterDataInterface)
// - LogicStructure (FilterSample)
// - LogicArray (coefficient storage)
// - Pipeline (pipelined MAC accumulation)
// - FiniteStateMachine (FilterController)
//
// The filter bank has two channels that share an identical MacUnit definition.
// A controller FSM sequences: idle → loading → running → draining → done.
//
// 2026 March 26
// Author: Desmond Kirkpatrick <desmond.a.kirkpatrick@intel.com>

import 'dart:async';

import 'package:rohd/rohd.dart';

// Import module definitions.
import 'package:rohd/src/examples/filter_bank_modules.dart';

// Re-export so downstream consumers (e.g. devtools loopback) can use.
export 'package:rohd/src/examples/filter_bank_modules.dart';

// ──────────────────────────────────────────────────────────────────
// Standalone simulation entry point
// ──────────────────────────────────────────────────────────────────

Future<void> main({bool noPrint = false}) async {
const dataWidth = 16;
const numTaps = 3;

// Low-pass-ish coefficients (scaled integers)
const coeffs0 = [1, 2, 1]; // channel 0: symmetric LPF kernel
const coeffs1 = [1, -2, 1]; // channel 1: high-pass kernel

final clk = SimpleClockGenerator(10).clk;
final reset = Logic(name: 'reset');
final start = Logic(name: 'start');
final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn');
final validIn = Logic(name: 'validIn');
final inputDone = Logic(name: 'inputDone');

final dut = FilterBank(
clk,
reset,
start,
samplesIn,
validIn,
inputDone,
numTaps: numTaps,
dataWidth: dataWidth,
coefficients: [coeffs0, coeffs1],
);

// Before we can simulate or generate code, we need to build it.
await dut.build();

// Set a maximum time for the simulation so it doesn't keep running forever.
Simulator.setMaxSimTime(500);

// Attach a waveform dumper so we can see what happens.
if (!noPrint) {
WaveDumper(dut, outputPath: 'filter_bank.vcd');
}

// Kick off the simulation.
unawaited(Simulator.run());

// ── Reset ──
reset.inject(1);
start.inject(0);
samplesIn.elements[0].inject(0);
samplesIn.elements[1].inject(0);
validIn.inject(0);
inputDone.inject(0);

await clk.nextPosedge;
await clk.nextPosedge;
reset.inject(0);

// ── Start filtering ──
await clk.nextPosedge;
start.inject(1);
await clk.nextPosedge;
start.inject(0);
validIn.inject(1);

// ── Feed sample stream: impulse response test ──
// Send a single '1' followed by zeros to get the impulse response
samplesIn.elements[0].inject(1);
samplesIn.elements[1].inject(1);
await clk.nextPosedge;

for (var i = 0; i < 8; i++) {
samplesIn.elements[0].inject(0);
samplesIn.elements[1].inject(0);
await clk.nextPosedge;
}

// ── Signal end of input ──
validIn.inject(0);
inputDone.inject(1);
await clk.nextPosedge;
inputDone.inject(0);

// ── Wait for drain ──
for (var i = 0; i < 15; i++) {
await clk.nextPosedge;
}

await Simulator.endSimulation();
}
Loading
Loading