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:
[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:
[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:
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.
#[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.
#[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.
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.
#[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.