Hardware in ROHD is convertible to an output format via Synthesizers, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained.

The simplest way to generate SystemVerilog text is with dumpSystemVerilog on Module:

void main() async {
    final myModule = MyModule();
    
    // remember that `build` returns a `Future`, hence the `await` here
    await myModule.build();

    final systemVerilog = myModule.dumpSystemVerilog();
    print(systemVerilog);
}

dumpSystemVerilog returns one string containing the SystemVerilog module definitions for the top-level module and all recursive submodules. For filesystem output, use SystemVerilogService and explicitly write its artifacts:

final systemVerilog = SystemVerilogService(
  myModule,
  outputDirectory: 'build',
  outputBaseName: 'myHardware',
);
systemVerilog.writeOutputs(); // Writes build/myHardware.sv.

To write one .sv file per module definition, configure the service with multiFile: true:

final systemVerilog = SystemVerilogService(
  myModule,
  outputDirectory: 'build/systemverilog',
  multiFile: true,
);
systemVerilog.writeOutputs();

For artifact streams or other explicit output configuration, use SystemVerilogService directly.

Generating a netlist

Generate a Yosys-compatible JSON netlist from a built top-level module with NetlistSynthesizer and SynthBuilder. generateCombinedJson emits one netlist document for the complete synthesized hierarchy. Replace MyModule with your own top-level module:

import 'dart:io';

import 'package:rohd/rohd.dart';

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

  final synthesizer = NetlistSynthesizer();
  final builder = SynthBuilder(myModule, synthesizer);
  final netlistJson = synthesizer.generateCombinedJson(builder, myModule);
  print(netlistJson);

  final outputFile = File('build/my_hardware.rohd.json');
  await outputFile.parent.create(recursive: true);
  await outputFile.writeAsString(netlistJson);
}

NetlistSynthesizerConfiguration controls optional netlist generation behavior, such as slim output for hierarchy loading. The same SynthBuilder also provides individual per-module synthesis results and generated file contents.

The JSON contains a creator, format version, and a modules map containing the complete synthesized hierarchy. Each module entry contains attributes, ports, cells, and netnames.

Controlling port types

Generated ports default to input logic, output logic, and inout wire, preserving the traditional ROHD declarations. Use a SystemVerilogSynthesizerConfiguration to independently control whether object types, such as wire and var, and data types, such as logic, are explicit for each port direction:

final systemVerilog = myModule.dumpSystemVerilog(
  configuration: const SystemVerilogSynthesizerConfiguration(
    inputPortType: SystemVerilogPortTypeConfiguration(
      objectType: SystemVerilogPortType.explicit,
      dataType: SystemVerilogPortType.implicit,
    ),
    outputPortType: SystemVerilogPortTypeConfiguration(
      objectType: SystemVerilogPortType.implicit,
      dataType: SystemVerilogPortType.implicit,
    ),
    inOutPortType: SystemVerilogPortTypeConfiguration(
      objectType: SystemVerilogPortType.implicit,
      dataType: SystemVerilogPortType.explicit,
    ),
  ),
);

The same configuration can be passed directly to SystemVerilogSynthesizer when using SynthBuilder.

Controlling naming

Modules

Port names are always maintained exactly in generated SystemVerilog, so they must always be unique and sanitary (valid) SystemVerilog.

Modules have two names:

  • The definitionName, which maps to the name of the module declaration in SystemVerilog.
    • If you want to ensure this does not change (e.g. uniquified because multiple different declarations have the same definitionname), set reserveDefinitionName to true.
  • The name, which maps to the instance name when that instance is instantiated as a sub-module of another module.
    • If you want to ensure this does not change (e.g. uniquified because other signals or sub-modules would have the same name), then set reserveName to true.

Internal signals

Internal signals, unlike ports, don’t need to always have the same exact name as in the original hardware definition.

  • If you do not name a signal, it will get a default name. Generated code will attempt to avoid keeping that intermediate signal around (declared) if possible.
  • If you do name a signal, by default it will be characterized as renameable. This means it will try to keep that name in generated output, but may rename it for uniquification purposes.
  • If you want to make sure an internal signal maintains exactly the name you want, you can mark it explicitly with reserved.
  • You can downgrade a named signal as well to mergeable or even unnamed, if you care less about its name in generated outputs and prefer that others will take over.

Connected, equivalent whole signals with compatible types may share one generated declaration when all their reserved or renameable names match exactly in the generated module’s namespace, before uniquification. For example, three connected signals named data, with any combination of these two naming modes, may generate just one data signal. This also applies when an internal signal connects to a port named data; the port name and module interface remain unchanged.

These modes preserve a generated name, not a separate declaration for every original signal. Structure-qualified names are compared, so matching leaf names alone are insufficient. Different preserved names remain separate, and matching names never establish connectivity: unrelated renameable signals are still uniquified, while unrelated reserved collisions still throw an exception. Partial assignments and incompatible array shapes are not eligible for this same-name merge.

Unpreferred names

The Naming.unpreferredName function will modify a signal name to indicate to downstream flows that the name is preferably omitted from the output, but preferable to an unnamed signal. This is generally most useful for things like output ports of InlineSystemVerilog modules.

Services API

ModuleService is the shared API for module-scoped generation, capture, and inspection. Services can register with ModuleServices for lookup by DevTools and other consumers. ArtifactProducingService implementations expose named artifacts as media-typed byte streams, so consumers do not need to require a local output file.

Registering and discovering services

Services such as SystemVerilogService and WaveformService register themselves by default when constructed. ModuleServices keeps the most recently registered service of each concrete type. This allows DevTools, application code, and other services to discover an optional capability without requiring the creator to pass the service instance to every consumer:

final systemVerilog = SystemVerilogService(myModule);

final registeredSystemVerilog =
    ModuleServices.instance.lookup<SystemVerilogService>();

assert(identical(systemVerilog, registeredSystemVerilog));
assert(identical(systemVerilog, SystemVerilogService.current));

Constructing another SystemVerilogService replaces the previous SystemVerilogService in the registry. Other service types remain registered. Use unregister<T>() to remove one service type or reset() to clear the registry.

Registration also enables services to collaborate without directly depending on how the application created them. For example, a generation or capture service can look up an optional tracing service and include source file, line, and column information when tracing is available. Because a service may consult the registry during construction or generation, register supporting services before constructing the services that consume them.

For one-shot work that should not change globally discoverable service state, set register to false:

final oneShotSystemVerilog = SystemVerilogService(
  myModule,
  register: false,
);

Module.dumpSystemVerilog() is a one-shot operation and uses register: false, so it does not change globally discoverable service state. Module.dumpWaves() registers its WaveformService by default. Construct the corresponding service directly with register: false when registration is not desired.

SystemVerilogService is the direct synthesis service. Its outputDirectory defaults to the current directory and its outputBaseName defaults to the top module’s definitionName. The service generates output in memory; call writeOutputs only when files are required:

final service = SystemVerilogService(
  myModule,
  outputDirectory: 'build/netlist',
  outputBaseName: 'accelerator',
  configuration: const SystemVerilogSynthesizerConfiguration(),
);

// Use the generated SystemVerilog directly.
final generatedSv = service.output;

// Or inspect a transport-neutral artifact stream.
final artifact = service.artifacts.single;
final bytes = await artifact.openRead().expand((chunk) => chunk).toList();

// Write build/netlist/accelerator.sv.
service.writeOutputs();

With multiFile: true, SystemVerilogService writes one .sv file per generated module definition. For custom synthesis flows, SynthBuilder accepts a Module and a Synthesizer (usually a SystemVerilogSynthesizer).

Capturing waveforms

Use dumpWaves for the legacy-compatible common case of writing all simulation signals to a VCD file:

myModule.dumpWaves(outputPath: 'waves.vcd');

WaveformService records VCD data in memory by default and exposes it as a ModuleServiceArtifact. Its output directory and basename use the same defaults as other artifact-producing services. Set writeToFile for a bounded-memory file-backed capture; set retainInMemory: true as well only when that file-backed capture also needs whole-history in-memory access:

final waveform = WaveformService(
  myModule,
  outputDirectory: 'build/waves',
  outputBaseName: 'interesting-signals',
  writeToFile: true,
  timescale: '1ns',
  startTime: 100,
  stopTime: 1000,
  signalFilter: (signal) => signal.name.startsWith('debug_'),
);

final vcdArtifact = waveform.artifacts.single;

Updated: