Interfaces

Interfaces make it easier to define port connections of a module in a reusable way. An example of the counter re-implemented using interfaces is shown below.

Interface takes a generic parameter for direction type. This enables you to group signals so make adding them as inputs/outputs easier for different modules sharing this interface.

The Port class extends Logic, but has a constructor that takes width as a positional argument to make interface port definitions a little cleaner.

When connecting an Interface to a Module, you should always create a new instance of the Interface so you don’t modify the one being passed in through the constructor. Modifying the same Interface as was passed would have negative consequences if multiple Modules were consuming the same Interface, and also breaks the rules for Module input and output connectivity.

The connectIO function under the hood calls addInput and addOutput directly on the Module and connects those Module ports to the correct ports on the Interfaces. Connection is based on signal names. You can use the uniquify Function argument in connectIO to uniquify inputs and outputs in case you have multiple instances of the same Interface connected to your module. You can also use the setPort function to directly set individual ports on the Interface instead of via tagged set of ports.

// Define a set of legal directions for this interface, and pass as parameter to Interface
enum CounterDirection {IN, OUT}
class CounterInterface extends Interface<CounterDirection> {

  // include the getters in the interface so any user can access them
  Logic get en => port('en');
  Logic get reset => port('reset');
  Logic get val => port('val');

  final int width;
  CounterInterface(this.width) {
    // register ports to a specific direction
    setPorts([
      Port('en'), // Port extends Logic
      Port('reset')
    ], [CounterDirection.IN]);  // inputs to the counter

    setPorts([
      Port('val', width),
    ], [CounterDirection.OUT]); // outputs from the counter
  }

}

class Counter extends Module {

  late final CounterInterface intf;
  Counter(CounterInterface intf) {
    // define a new interface, and connect it to the interface passed in
    this.intf = CounterInterface(intf.width)
      ..connectIO(this, intf,
        // map inputs and outputs to appropriate directions
        inputTags: {CounterDirection.IN},
        outputTags: {CounterDirection.OUT}
      );

    _buildLogic();
  }

  void _buildLogic() {
    var nextVal = Logic(name: 'nextVal', width: intf.width);

    // access signals directly from the interface
    nextVal <= intf.val + 1;

    Sequential( SimpleClockGenerator(10).clk, [
      If(intf.reset, then:[
        intf.val < 0
      ], orElse: [If(intf.en, then: [
        intf.val < nextVal
      ])])
    ]);
  }
}

Pair Interfaces

A typical use case for interfaces is for two components to talk to each other, with some shared system inputs like clocks, resets, etc. Additionally, interfaces may be broken down into sub-interfaces. The PairInterface class can help automate a lot of the boilerplate for these types of common use cases.

A simple interface with a clock, request, and response might look like this:

class SimpleInterface extends PairInterface {
  Logic get clk => port('clk');
  Logic get req => port('req');
  Logic get rsp => port('rsp');

  SimpleInterface()
      : super(
          portsFromConsumer: [Port('rsp')],
          portsFromProducer: [Port('req')],
          sharedInputPorts: [Port('clk')],
        );

  SimpleInterface.clone(SimpleInterface super.otherInterface) : super.clone();
}

Note that it comes with helpers in the super constructor for grouping ports as well as cloning the interface. Using this interface in a simple provider/consumer scenario, even with module hierarchy is easy. You can use the pairConnectIO function which references the “role” of the component rather than listing input and output tags explicitly.

class SimpleProvider extends Module {
  late final SimpleInterface _intf;
  SimpleProvider(SimpleInterface intf) {
    _intf = SimpleInterface.clone(intf)
      ..pairConnectIO(this, intf, PairRole.provider);

    SimpleSubProvider(_intf);
  }
}

class SimpleSubProvider extends Module {
  late final SimpleInterface _intf;
  SimpleSubProvider(SimpleInterface intf) {
    _intf = SimpleInterface.clone(intf)
      ..pairConnectIO(this, intf, PairRole.provider);
  }
}

class SimpleConsumer extends Module {
  late final SimpleInterface _intf;
  SimpleConsumer(SimpleInterface intf) {
    _intf = SimpleInterface.clone(intf)
      ..pairConnectIO(this, intf, PairRole.consumer);
  }
}

class SimpleTop extends Module {
  SimpleTop(Logic clk) {
    clk = addInput('clk', clk);
    final intf = SimpleInterface();
    intf.clk <= clk;
    SimpleConsumer(intf);
    SimpleProvider(intf);
  }
}

You can easily add interface hierarchy with the addSubInterface function. For example:

class SubInterface extends PairInterface {
  Logic get rsp => port('rsp');
  Logic get req => port('req');

  SubInterface()
      : super(
          portsFromConsumer: [Port('rsp')],
          portsFromProvider: [Port('req')],
        );
  SubInterface.clone(SubInterface super.otherInterface) : super.clone();
}

class TopLevelInterface extends PairInterface {
  Logic get clk => port('clk');

  final int numSubInterfaces;

  final List<SubInterface> subIntfs = [];

  TopLevelInterface(this.numSubInterfaces)
      : super(
          sharedInputPorts: [Port('clk')],
        ) {
    for (var i = 0; i < numSubInterfaces; i++) {
      subIntfs.add(addSubInterface('sub$i', SubInterface()));
    }
  }

  TopLevelInterface.clone(TopLevelInterface otherInterface)
      : this(otherInterface.numSubInterfaces);
}

There are some other utilities available in PairInterface as well, such as the ability to reverse sub-interfaces. Check out the API docs for full details.

Updated: