Skip to main content

Quickstart

To get you up to speed with Substrate's basic features, we'll show you how make a simple voltage divider schematic.

In this tutorial, we'll breeze through the basics to give you a sense of what Substrate generators look like. If you're looking for a deeper dive into how analog design and simulation work in Substrate, check out the Designing an inverter tutorial.

Creating a Substrate project

Substrate is fully integrated with the Rust ecosystem, so all you need to get started is a recent installation of Rust! Ensure that you have version 1.70.0 or beyond.

First, add the Substrate registry to your Cargo config:

~/.cargo/config.toml
[registries]
substrate = { index = "https://github.com/substrate-labs/crates-index" }

You only need to do this the first time you set up Substrate.

Next, create a new Rust project:

cargo new --lib my_generator && cd my_generator

In your project's Cargo.toml, add the following dependencies:

Cargo.toml
[dependencies]
substrate = { version = "0.8.1", registry = "substrate" }
spice = { version = "0.7.1", registry = "substrate" }

serde = { version = "1", features = ["derive"] }
rust_decimal = "1.30"
rust_decimal_macros = "1.30"

Let's now add some imports that we'll use later on. Replace the content of src/lib.rs with the following:

src/lib.rs
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use spice::Spice;
use substrate::block::Block;
use substrate::io::{InOut, Io, Output, SchematicType, Signal};
use substrate::schematic::primitives::Resistor;
use substrate::schematic::{CellBuilder, ExportsNestedData, Schematic};

Interface

We'll first define the interface (also referred to as IO) exposed by our voltage divider.

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

Voltage divider parameters

Now that we've defined an IO, we can define a block. Substrate blocks are analogous to modules or cells in other generator frameworks.

src/lib.rs
#[derive(Serialize, Deserialize, Block, Debug, Copy, Clone, Hash, PartialEq, Eq)]
#[substrate(io = "VdividerIo")]
pub struct Vdivider {
/// The top resistance.
pub r1: Decimal,
/// The bottom resistance.
pub r2: Decimal,
}

Schematic generator

We now define the schematic of the voltage divider.

src/lib.rs
impl ExportsNestedData for Vdivider {
type NestedData = ();
}

impl Schematic<Spice> for Vdivider {
fn schematic(
&self,
io: &<<Self as Block>::Io as SchematicType>::Bundle,
cell: &mut CellBuilder<Spice>,
) -> substrate::error::Result<Self::NestedData> {
let r1 = cell.instantiate(Resistor::new(self.r1));
let r2 = cell.instantiate(Resistor::new(self.r2));

cell.connect(io.vdd, r1.io().p);
cell.connect(io.dout, r1.io().n);
cell.connect(io.dout, r2.io().p);
cell.connect(io.vss, r2.io().n);

Ok(())
}
}

Writing the netlist

We can now write a Rust unit test to write the netlist to a file.

lib/tb.rs
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use spice::netlist::NetlistOptions;
use std::path::PathBuf;
use substrate::context::Context;

#[test]
pub fn netlist_vdivider() {
let ctx = Context::new();
Spice
.write_block_netlist_to_file(
&ctx,
Vdivider {
r1: dec!(100),
r2: dec!(200),
},
PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/netlist_vdivider"
))
.join("vdivider.spice"),
NetlistOptions::default(),
)
.expect("failed to netlist vdivider");
}
}

To run the test, run

cargo test netlist_vdivider

Conclusion

If all goes well, the test above should write the voltage divider netlist to tests/netlist_vdivider/vdivider.spice. A full, runnable example for this tutorial is available here.