Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.bwrc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,3 @@ itertools = "0.14"
[features]
commercial = ["dep:calibre", "dep:abstract_lef", "dep:liberate_mx", "dep:spectre", "dep:sky130_commercial_pdk", "dep:sub_calibre"]
default = ["commercial"]

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ paste = "1"

[features]
default = []

40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ If you have BWRC access, you can install all features of SRAM22. Make sure that
```
[net]
git-fetch-with-cli = true
[registries]
substrate = { index = "https://github.com/substrate-labs/crates-index" }
```

You can then install SRAM22 using the following commands:
Expand Down Expand Up @@ -61,38 +63,45 @@ Options:
-c, --config <CONFIG> Path to TOML configuration file [default: sram22.toml]
-o, --output-dir <OUTPUT_DIR> Directory to which output files should be saved
--lef Generate LEF (used in place and route)
--lib Generate LIB (setup, hold, and delay timing information)
--liberate Generate LIB with Liberate MX instead of the interpolation model
--drc Run DRC using Calibre
--lvs Run LVS using Calibre
--pex Run PEX using Calibre
-a, --all Run all available steps
-h, --help Print help information
-V, --version Print version information
```

`--liberate`, `--drc`, `--lvs`, and `--all` are only available with a full (commercial) installation.

### Configuration

SRAM22 generates memory blocks based on a TOML configuration file. An example configuration, showing all the available options, is shown below:
SRAM22 generates memory blocks based on a TOML configuration file. Configurations are specified
as an array of `[[sram]]` configurations, allowing up to multiple SRAMs to be generated.

```toml
[[sram]]
num_words = 64
data_width = 32
mux_ratio = 4
write_size = 8

[[sram]]
num_words = 256
data_width = 64
mux_ratio = 4
write_size = 8
# The `pex_level` flag is only available with a full installation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this deleted from the docs?

pex_level = "rcc"
Comment thread
rohanku marked this conversation as resolved.
```

To generate an SRAM using this configuration, put the above text into a file called
`sram22_64x32m4w8/sram22.toml`, then run:
Save this as `sram22.toml` and run:

```
cd sram22_64x32m4w8
sram22
```

Add additional flags depending on what views you want to generate and what verification you want to run.
If you do not have access to BWRC servers, most flags will not be available.
Each `[[sram]]` block is generated independently. Output files are placed in subdirectories
named after the SRAM (e.g. `build/sram22_64x32m4w8/`)

The number of rows in the SRAM bitcell array is `num_words / mux_ratio`.
The number of columns in the array is `data_width * mux_ratio`.
Expand All @@ -103,7 +112,20 @@ A valid configuration must have:
* A power-of-two number of rows
* At least 16 rows
* At least 16 columns
* `pex_level`: Must be `"r"`, `"c"`, `"rc"`, or `"rcc"`. If you do not have commercial plugins enabled, this option will be ignored.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you take this out?

* `pex_level` (optional): Must be `"r"`, `"c"`, `"rc"`, or `"rcc"`. Only available with a full installation. When set, PEX runs automatically — no additional flag required. Each `[[sram]]` block sets its own `pex_level` independently; SRAMs without it skip PEX. The extracted netlist is only consumed by Liberate MX (see below).

### LIB generation

SRAM22 always generates Liberty (.lib) timing files for the tt/ss/ff PVT corners — no flag
is required. By default the LIB is produced by an open-source interpolation model;
interpolated LIBs carry a conservative overestimate of up to 2% for SRAM configurations
with `data_width` between 8 and 128. `data_width` outside that range is not supported by
the open-source model and will produce an error.

With a full BWRC installation, passing `--liberate` (or `--all`) instead characterizes the
LIB with Liberate MX running SPICE simulation; interpolation is skipped in that case. If a
config also has `pex_level` set, Liberate MX uses the extracted netlist; otherwise it falls
back to the plain SPICE netlist.

### Contribution

Expand Down
17 changes: 17 additions & 0 deletions src/blocks/sram/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ pub fn parse_sram_config(path: impl AsRef<Path>) -> anyhow::Result<SramConfig> {
Ok(data)
}

#[derive(Debug, Deserialize)]
pub struct SramBatchConfig {
pub sram: Vec<SramConfig>,
}

pub fn parse_sram_batch_config(path: impl AsRef<Path>) -> anyhow::Result<Vec<SramConfig>> {
let contents = std::fs::read_to_string(&path)?;
if let Ok(batch) = toml::from_str::<SramBatchConfig>(&contents) {
if !batch.sram.is_empty() {
return Ok(batch.sram);
}
}
let single = toml::from_str::<SramConfig>(&contents)
.map_err(|e| anyhow::anyhow!("Failed to parse config: {}", e))?;
Ok(vec![single])
}

pub struct SramInner {
params: SramParams,
}
Expand Down
10 changes: 3 additions & 7 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ pub struct Args {
#[arg(short, long)]
pub output_dir: Option<PathBuf>,

/// Generate LIB (setup, hold, and delay timing information).
/// Generate LIB timing using Liberate MX SPICE characterization instead of
/// the open-source interpolation model (requires a full installation).
#[cfg(feature = "commercial")]
#[arg(long)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should generate LIB by interpolation by default, there should be a flag to choose to generate using liberate that is only available on the commercial feature flag.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By this I meant you should delete this flag and only have the liberate flag. It always produces a LIB, but only invokes liberate if --liberate is specified. Interpolation shouldn't run if liberate is being used to generate the lib.

pub lib: bool,
pub liberate: bool,

/// Run DRC using Calibre.
#[cfg(feature = "commercial")]
Expand All @@ -36,11 +37,6 @@ pub struct Args {
#[arg(long)]
pub lvs: bool,

/// Run PEX using Calibre.
#[cfg(feature = "commercial")]
#[arg(long)]
pub pex: bool,

#[cfg(feature = "commercial")]
/// Run all available steps.
#[arg(short, long)]
Expand Down
205 changes: 154 additions & 51 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,62 @@
use std::collections::HashSet;
use std::fs::canonicalize;
use std::path::PathBuf;
use std::sync::Arc;

use clap::Parser;

use crate::blocks::sram::parse_sram_config;
use indicatif::MultiProgress;

use crate::blocks::sram::{parse_sram_batch_config, SramConfig};
use crate::cli::args::Args;
use crate::cli::progress::StepContext;
use crate::plan::{execute_plan, generate_plan, ExecutePlanParams, TaskKey};
use crate::paths::{out_gds, out_lef, out_spice, out_verilog};
use crate::plan::{execute_plan, generate_plan, ExecutePlanParams, SramPlan, TaskKey};
use crate::Result;

pub mod args;
pub mod progress;

pub const BANNER: &str = r"
________ ________ ________ _____ ______ _______ _______
|\ ____\|\ __ \|\ __ \|\ _ \ _ \ / ___ \ / ___ \
\ \ \___|\ \ \|\ \ \ \|\ \ \ \\\__\ \ \/__/|_/ //__/|_/ /|
\ \_____ \ \ _ _\ \ __ \ \ \\|__| \ \__|// / /__|// / /
\|____|\ \ \ \\ \\ \ \ \ \ \ \ \ \ \ / /_/__ / /_/__
________ ________ ________ _____ ______ _______ _______
|\ ____\|\ __ \|\ __ \|\ _ \ _ \ / ___ \ / ___ \
\ \ \___|\ \ \|\ \ \ \|\ \ \ \\\__\ \ \/__/|_/ //__/|_/ /|
\ \_____ \ \ _ _\ \ __ \ \ \\|__| \ \__|// / /__|// / /
\|____|\ \ \ \\ \\ \ \ \ \ \ \ \ \ \ / /_/__ / /_/__
____\_\ \ \__\\ _\\ \__\ \__\ \__\ \ \__\|\________\\________\
|\_________\|__|\|__|\|__|\|__|\|__| \|__| \|_______|\|_______|
\|_________|
\|_________|


SRAM22 v0.2
";

fn is_already_built(work_dir: &std::path::Path, name: &str) -> bool {
// Completeness is judged on the layout artifacts only, not the LIB. A LIB is
// interpolated by default when possible, but not every SRAM config has timing
// data to interpolate from, so a missing .lib does not mean the build is stale.
out_spice(work_dir, name).exists()
&& out_gds(work_dir, name).exists()
&& out_verilog(work_dir, name).exists()
&& out_lef(work_dir, name).exists()
}

/// Layer per-config tasks onto the shared base set. A config runs PEX exactly
/// when it specifies a `pex_level`.
#[cfg(feature = "commercial")]
fn config_tasks(base: &HashSet<TaskKey>, config: &SramConfig) -> Arc<HashSet<TaskKey>> {
let mut tasks = base.clone();
if config.pex_level.is_some() {
tasks.insert(TaskKey::RunPex);
}
Arc::new(tasks)
}

#[cfg(not(feature = "commercial"))]
fn config_tasks(base: &HashSet<TaskKey>, _config: &SramConfig) -> Arc<HashSet<TaskKey>> {
Arc::new(base.clone())
}

pub fn run() -> Result<()> {
let args = Args::parse();

Expand All @@ -35,59 +65,132 @@ pub fn run() -> Result<()> {
println!("{BANNER}");

println!("Reading configuration file...\n");
let config = parse_sram_config(&config_path)?;
let configs = parse_sram_batch_config(&config_path)?;

println!("Configuration file: {:?}", &config_path);
println!("SRAM parameters:");
println!("\tNumber of words: {}", config.num_words);
println!("\tData width: {}", config.data_width);
println!("\tMux ratio: {}", config.mux_ratio as usize);
println!("\tWrite size: {}", config.write_size);
let config_dir = config_path
.parent()
.ok_or_else(|| anyhow::anyhow!("Config path has no parent directory"))?;

let build_dir = if let Some(output_dir) = args.output_dir {
output_dir
} else {
config_dir.join("build")
};
std::fs::create_dir_all(&build_dir)?;
let build_dir = canonicalize(build_dir)?;

let enabled_tasks = vec![
// Every run generates a LIB; DRC, LVS, and the `all` umbrella are opt-in.
// PEX is decided per config in `config_tasks` from each config's pex_level.
let base_tasks: HashSet<TaskKey> = [
(true, TaskKey::GenerateLib),
#[cfg(feature = "commercial")]
(args.drc, TaskKey::RunDrc),
#[cfg(feature = "commercial")]
(args.lvs, TaskKey::RunLvs),
#[cfg(feature = "commercial")]
(
args.pex || (args.lib && config.pex_level.is_some()),
TaskKey::RunPex,
),
#[cfg(feature = "commercial")]
(args.lib, TaskKey::GenerateLib),
#[cfg(feature = "commercial")]
(args.all, TaskKey::All),
]
.into_iter()
.filter_map(|(a, b)| if a { Some(b) } else { None });

let tasks = HashSet::from_iter(enabled_tasks);

let mut ctx = StepContext::new(&tasks);
.filter_map(|(enabled, task)| enabled.then_some(task))
.collect();

let plan = ctx.check(generate_plan(&config))?;
ctx.finish(TaskKey::GeneratePlan);
let plans: Vec<SramPlan> = configs
.iter()
.map(|c| generate_plan(c))
.collect::<Result<Vec<_>>>()?;

let work_dir = if let Some(output_dir) = args.output_dir {
output_dir
} else {
PathBuf::from(plan.sram_params.name().as_str())
};
std::fs::create_dir_all(&work_dir)?;
let work_dir = canonicalize(work_dir)?;

let res = execute_plan(ExecutePlanParams {
work_dir: &work_dir,
plan: &plan,
tasks: &tasks,
ctx: Some(&mut ctx),
#[cfg(feature = "commercial")]
pex_level: config.pex_level,
});

ctx.check(res)?;
println!("Artifacts saved to: {:?}\n", &work_dir);
println!("Configuration file: {:?}", &config_path);
for (i, (config, plan)) in configs.iter().zip(plans.iter()).enumerate() {
println!(
" [{}] {} (num_words={}, data_width={}, mux_ratio={}, write_size={})",
i + 1,
plan.sram_params.name(),
config.num_words,
config.data_width,
config.mux_ratio as usize,
config.write_size,
);
}
println!();

let mp = MultiProgress::new();

let handles: Vec<_> = plans
.into_iter()
.zip(configs.into_iter())
.filter_map(|(plan, config)| {
let work_dir_check = build_dir.join(plan.sram_params.name().as_str());
if is_already_built(&work_dir_check, &plan.sram_params.name()) {
return None;
}

let tasks = config_tasks(&base_tasks, &config);

let mut ctx = StepContext::new_with_mp(&tasks, mp.clone(), &plan.sram_params.name());
ctx.finish(TaskKey::GeneratePlan);

let build_dir = build_dir.clone();
Some(std::thread::spawn(move || -> (Result<PathBuf>, StepContext) {
let result = (|| -> Result<PathBuf> {
let work_dir = build_dir.join(plan.sram_params.name().as_str());
std::fs::create_dir_all(&work_dir)?;
let work_dir = canonicalize(work_dir)?;
let res = execute_plan(ExecutePlanParams {
work_dir: &work_dir,
plan: &plan,
tasks,
ctx: Some(&mut ctx),
#[cfg(feature = "commercial")]
pex_level: config.pex_level,
#[cfg(feature = "commercial")]
use_liberate: args.liberate || args.all,
});
ctx.check(res)?;
Ok(work_dir)
})();
(result, ctx)
}))
})
.collect();

// Join ALL threads first, then commit ALL progress bars together to avoid
// ghost snapshots that appear when one bar finishes while others are live.
let joined: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
let mut errors: Vec<anyhow::Error> = Vec::new();
let mut work_dirs: Vec<PathBuf> = Vec::new();
for join_result in joined {
match join_result {
Ok((Ok(work_dir), mut ctx)) => {
ctx.commit();
work_dirs.push(work_dir);
}
Ok((Err(e), mut ctx)) => {
ctx.commit();
errors.push(e);
}
Err(e) => {
let msg = e
.downcast_ref::<String>()
.map(|s| s.as_str())
.or_else(|| e.downcast_ref::<&'static str>().copied())
.unwrap_or("(no message)");
errors.push(anyhow::anyhow!("SRAM generation thread panicked: {}", msg));
}
}
}
for work_dir in work_dirs {
println!("Artifacts saved to: {:?}", work_dir);
}

if !errors.is_empty() {
let msg = errors
.iter()
.enumerate()
.map(|(i, e)| format!(" [{}] {:#}", i + 1, e))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!("{} SRAM(s) failed:\n{}", errors.len(), msg);
}

Ok(())
}
Loading