Skip to main content
Version: main

IOs

In this section, we'll explore how to define and use the interfaces between generated schematics.

Defining an IO

The first step in creating a Substrate schematic generator is to define an interface that other generators can use to instantiate your generator. An interface, called an IO in Substrate, defines a set of ports and their directions.

#[derive(Io, Clone, Default, Debug)]
pub struct VdividerIo {
pub vdd: InOut<Signal>,
pub vss: InOut<Signal>,
pub dout: Output<Signal>,
}

An IO must implement the Io trait. Implementing this trait is most easily done by using #[derive(Io)].

Bundle kinds

The IO struct itself does not store any connectivity data, but rather is a template for what an instantiation of the IO should look like. Specifically, an IO struct has an associated directionality and bundle kind.

Directionality means that each signal in the IO has an associated direction (input, output, or inout). A bundle kind is an undirectioned template describing what data is stored in its associated bundles. VdividerIo::default(), for example, would describe an IO template where the dout port should be an output signal. VdividerIo has bundle kind VdividerIoKind, which is auto-generated by #[derive(Io)]. The bundle kind declares that dout should be a single-bit signal, but does not declare the direction.

If we want to declare a bundle kind that does not have inherent directions, we can use the #[derive(BundleKind)] macro.

#[derive(BundleKind, Clone, Default, Debug, PartialEq, Eq)]
pub struct DiffPair {
pub p: Signal,
pub n: Signal,
}

We can then use the IOs Input<DiffPair>, Output<DiffPair>, and InOut<DiffPair>, which all have bundle kind DiffPair.

Runtime configuration

Some bundle kinds can be configured at runtime. For example, we can also describe an IO containing two 5-bit buses as follows:

#[derive(Io, Clone, Debug)]
pub struct ArrayIo {
pub in_bus: Input<Array<Signal>>,
pub out_bus: Output<Array<Signal>>,
}

let io_type = ArrayIo {
in_bus: Input(Array::new(5, Signal::new())),
out_bus: Output(Array::new(5, Signal::new())),
};

This allows you to parametrize the contents of your interface at runtime in whatever way you like. For example, we can do some calculations in the constructor for ArrayIo:

impl ArrayIo {
pub fn new(in_size: usize, m: usize) -> Self {
Self {
in_bus: Input(Array::new(in_size, Signal::new())),
out_bus: Output(Array::new(in_size * m, Signal::new())),
}
}
}

Port directions

Since port direction rules are often broken in analog design, Substrate does not enforce any directionality checks when connecting two ports. However, Substrate does run a basic driver analysis that throws warnings if there are multiple drivers of a net or no drivers, which may be helpful for debugging purposes.

All IOs implement the Directed trait, which allows them to specify the direction of each of their constituent ports. Wrapping a signal with one of the Input, Output, or InOut wrapper types allows you to specify the direction of different components of the IO. All IO ports must have a specified direction.

Wrapping a composite type with a direction will overwrite the direction of all constituent signals. In the example below, all of the ports of SramObserverIo are inputs.

#[derive(Io, Clone, Debug)]
pub struct SramIo {
pub clk: Input<Signal>,
pub we: Input<Signal>,
pub addr: Input<Array<Signal>>,
pub din: Input<Array<Signal>>,
pub dout: Output<Array<Signal>>,
}

pub type SramObserverIo = Input<SramIo>;

Similarly, if we wanted to create an SramDriverIo that drives the input signals of an SRAM and reads the output, we can use the Flipped wrapper type, which flips the direction of each constituent port.

pub type SramDriverIo = Flipped<SramIo>;

Bundles

Since bundle kind structs only define the properties of an interface, a separate struct is needed to store connectivity data for the signals and buses defined by the IO struct. This struct is called a bundle and is associated with an IO struct via the SchematicBundleKind trait.

A bundle essentially just stores what each port in the IO is connected to and is created when the schematic type described by an IO struct is instantiated. In the case of the VdividerIo given before, the #[derive(Io)] macro automatically generates an appropriate schematic bundle that looks something like this:

pub struct VdividerIoView<NodeBundle> {
pub vdd: Node,
pub vss: Node,
pub dout: Node,
}

While IO structs describe the type of an interface, bundles describe the data of an interface and represent the physical wires (Nodes) in a netlist. As such, bundles can be connected to one another and probed during simulation. The bundle kind of a bundle can be found using HasBundleKind::kind.

Connections

Substrate can connect any two bundles that have the same bundle kind. For example, two bundles with kind Array::new(5) can be connected to one another, but a bundle with kind Array::new(5) cannot be connected to a bundle with kind Array::new(6).

Connections are made between two bundles by flattening bundles into an array of constituent wires and connecting these wires in order. As such, only bundles of the same bundle kind can be connected since Substrate cannot make any assumptions on the ordering of wires in different bundle kinds.

Custom connections

Sometimes, you might want to connect bundles of different kinds. By default, Substrate does not allow this because it cannot infer how signals should be connected. However, it does allow you to define DataViews to explicitly declare how two bundles of different kinds should be connected.

Suppose we have the following two IOs:

#[derive(Io, Clone, Default, Debug)]
pub struct ThreePortMosIo {
pub d: InOut<Signal>,
pub g: Input<Signal>,
pub s: InOut<Signal>,
}

#[derive(Io, Clone, Default, Debug)]
pub struct FourPortMosIo {
pub d: InOut<Signal>,
pub g: Input<Signal>,
pub s: InOut<Signal>,
pub b: InOut<Signal>,
}

The following code can be used to define a conversion from one IO's bundle to the other:

impl DataView<FourPortMosIoKind> for ThreePortMosIoKind {
fn view_nodes_as(nodes: &NodeBundle<Self>) -> NodeBundle<FourPortMosIoKind> {
NodeBundle::<FourPortMosIoKind> {
d: nodes.d,
g: nodes.g,
s: nodes.s,
b: nodes.s,
}
}
}

We can then connect the corresponding bundles as follows:

let three_port_signal = cell.signal("three_port_signal", ThreePortMosIo::default());
let four_port_signal = cell.signal("four_port_signal", FourPortMosIo::default());
cell.connect(
three_port_signal.view_as::<FourPortMosIo>(),
four_port_signal,
);

However, sometimes, we might want to tie the body port to a separate node. We can do this using an auxiliary bundle kind:

#[derive(BundleKind, Clone, Default, Debug, PartialEq, Eq)]
pub struct WithBody<T> {
inner: T,
b: Signal,
}

impl DataView<FourPortMosIoKind> for WithBody<ThreePortMosIoKind> {
fn view_nodes_as(nodes: &NodeBundle<Self>) -> NodeBundle<FourPortMosIoKind> {
NodeBundle::<FourPortMosIoKind> {
d: nodes.inner.d,
g: nodes.inner.g,
s: nodes.inner.s,
b: nodes.b,
}
}
}

We can then connect the desired signals:

let three_port_signal = cell.signal("three_port_signal", ThreePortMosIo::default());
let body = cell.signal("body", Signal);
let four_port_signal = cell.signal("four_port_signal", FourPortMosIo::default());
cell.connect(
NodeBundle::<WithBody<ThreePortMosIoKind>> {
inner: three_port_signal,
b: body,
}
.view_as::<FourPortMosIo>(),
four_port_signal,
);

Arbitrary connections

If you would like to connect two bundles of different kinds and know that they can be connected without any reordering/re-mapping of signals, you can flatten both to arrays and connect them. This is not recommended since it is almost always better to use an explicit data view, but it can come in handy when you just want to test something quickly or you do not really care about the order of the bits.

For example, suppose we have two IOs that have the same constituent signals, but are different kinds (perhaps they were defined in different crates):

#[derive(Io, Clone, Default, Debug)]
pub struct ThreePortMosIo {
pub d: InOut<Signal>,
pub g: Input<Signal>,
pub s: InOut<Signal>,
}

#[derive(Io, Clone, Default, Debug)]
pub struct OtherThreePortMosIo {
pub drain: InOut<Signal>,
pub gate: Input<Signal>,
pub source: InOut<Signal>,
}

We can connect bundles of these IOs as follows:

let three_port_signal = cell.signal("three_port_signal", ThreePortMosIo::default());
let other_three_port_signal =
cell.signal("other_three_port_signal", OtherThreePortMosIo::default());
cell.connect(
three_port_signal.view_as::<Array<Signal>>(),
other_three_port_signal.view_as::<Array<Signal>>(),
);