Skip to content
Merged
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,23 @@ Then, run the following from the `rs/` folder:
cargo t
```

That runs every bench against every abstraction level of the analog models. To
iterate against the cheap one only:

```bash
cargo t eye
```

## Organization

Chisel RTL for all digital components can be found in the `scala/` directory.

Verilog testbenches and AMS models can be found in the `verilog/` folder.
Verilog testbenches and AMS models can be found in the `verilog/` folder. The
analog PHY is modelled at more than one level of abstraction there --
`verilog/models/eye` is the least that puts an eye in front of the receiver and
so the least that can be trained against, `verilog/models/circuit` is the front
end as it is built -- and `verilog/README.md` describes the levels, the contract
they share, and how a simulation picks one.

Rust code for orchestrating tests can be found in the `rs/` folder.

Expand Down
126 changes: 126 additions & 0 deletions rs/src/verilog/afe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Benches for the analog front end cells every abstraction level provides.
//!
//! These come from `verilog/common/afe_tb.vams`, are written against the model
//! contract rather than any one level's implementation of it, and so run at
//! every level. A cell that passes here can be swapped for another level's
//! version of it without anything upstream noticing.

/// The cells every level has to define, with the pins `verilog/README.md`
/// lists. Everything else in a level's directory is its own business.
pub const CONTRACT_CELLS: [&str; 7] = [
"tx_tile_driver",
"pad_driver_cell",
"termination",
"rdac",
"rx_afe",
"dcdl",
"clocking_distribution_model",
];

#[cfg(test)]
mod tests {
use anyhow::Result;
use regex::Regex;
use test_log::test;

use super::CONTRACT_CELLS;
use crate::verilog::{Level, model_src_files, tests::expect_clean};

/// A level that is missing a cell fails to elaborate against a testbench
/// that instantiates it, which is a confusing way to find out. This says so
/// directly, and without a simulator.
#[test]
fn every_level_defines_every_contract_cell() -> Result<()> {
for level in Level::ALL {
let sources = model_src_files(level)
.iter()
.map(std::fs::read_to_string)
.collect::<std::io::Result<Vec<_>>>()?
.join("\n");
for cell in CONTRACT_CELLS {
let declaration = Regex::new(&format!(r"(?m)^module\s+{cell}\b"))?;
assert!(
declaration.is_match(&sources),
"the {} level does not define `{cell}`",
level.dir_name()
);
}
}
Ok(())
}

// Benches written against the model contract in `verilog/README.md`
// have to hold at every level, so each runs at both. The level is in
// the test name rather than a module around it, which keeps
// `cargo test eye` selecting the fast half.

#[test]
fn rdac_eye() -> Result<()> {
expect_clean(Level::Eye, "rdac_tb")
}

#[test]
fn rdac_circuit() -> Result<()> {
expect_clean(Level::Circuit, "rdac_tb")
}

#[test]
fn pad_driver_data_eye() -> Result<()> {
expect_clean(Level::Eye, "pad_driver_data_tb")
}

#[test]
fn pad_driver_data_circuit() -> Result<()> {
expect_clean(Level::Circuit, "pad_driver_data_tb")
}

#[test]
fn pad_driver_impedance_eye() -> Result<()> {
expect_clean(Level::Eye, "pad_driver_impedance_tb")
}

#[test]
fn pad_driver_impedance_circuit() -> Result<()> {
expect_clean(Level::Circuit, "pad_driver_impedance_tb")
}

#[test]
fn tx_tile_driver_data_eye() -> Result<()> {
expect_clean(Level::Eye, "tx_tile_driver_data_tb")
}

#[test]
fn tx_tile_driver_data_circuit() -> Result<()> {
expect_clean(Level::Circuit, "tx_tile_driver_data_tb")
}

#[test]
fn tx_tile_driver_impedance_eye() -> Result<()> {
expect_clean(Level::Eye, "tx_tile_driver_impedance_tb")
}

#[test]
fn tx_tile_driver_impedance_circuit() -> Result<()> {
expect_clean(Level::Circuit, "tx_tile_driver_impedance_tb")
}

#[test]
fn rx_afe_eye() -> Result<()> {
expect_clean(Level::Eye, "rx_afe_tb")
}

#[test]
fn rx_afe_circuit() -> Result<()> {
expect_clean(Level::Circuit, "rx_afe_tb")
}

#[test]
fn termination_eye() -> Result<()> {
expect_clean(Level::Eye, "termination_tb")
}

#[test]
fn termination_circuit() -> Result<()> {
expect_clean(Level::Circuit, "termination_tb")
}
}
115 changes: 109 additions & 6 deletions rs/src/verilog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,96 @@ use std::{
use anyhow::{Context, Result, anyhow, bail};
use const_format::concatcp;

pub mod afe;
pub mod phy;
pub mod primitives;
pub mod rx;
pub mod tx;
pub mod serdes;
pub mod training;

pub const VERILOG_SRC_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../verilog");
pub const CONSTANTS: &str = concatcp!(VERILOG_SRC_DIR, "/constants.vams");
pub const COMMON_DIR: &str = concatcp!(VERILOG_SRC_DIR, "/common");
pub const MODELS_DIR: &str = concatcp!(VERILOG_SRC_DIR, "/models");
pub const XCELIUM_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../xcelium");
pub const CONTROL_FILE: &str = concatcp!(XCELIUM_DIR, "/amscf.scs");
pub const PROBE_FILE: &str = concatcp!(XCELIUM_DIR, "/probe.tcl");

pub fn get_src_files() -> Vec<PathBuf> {
["sv", "v", "vams"]
/// Abstraction level of the analog models the PHY is simulated against.
///
/// Every level provides the same analog cells with the same pins -- the
/// contract is written down in `verilog/README.md` -- so a level is selected by
/// nothing more than which directory under `verilog/models` is compiled
/// alongside `verilog/common`. Two levels cannot be compiled together, since
/// they define the same modules.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Level {
/// Highest abstraction: finite driver slew and a reference comparison, and
/// no more than that. Enough to open an eye in front of the receiver and
/// train against it, and cheap enough to do it in a full-stack simulation.
Eye,
/// Lowest abstraction: the front end as it is built, down to the
/// switched-capacitor sampler and the segments of the drivers.
Circuit,
}

impl Level {
/// Every level, cheapest first. Tests that are meant to hold at any level
/// iterate this.
pub const ALL: [Level; 2] = [Level::Eye, Level::Circuit];

pub fn dir_name(self) -> &'static str {
match self {
Level::Eye => "eye",
Level::Circuit => "circuit",
}
}

pub fn dir(self) -> PathBuf {
PathBuf::from(MODELS_DIR).join(self.dir_name())
}
}

fn sources_under(dir: impl AsRef<Path>) -> Vec<PathBuf> {
let dir = dir.as_ref().display().to_string();
let mut files: Vec<PathBuf> = ["sv", "v", "vams"]
.iter()
.flat_map(|ext| {
let pattern = format!("{VERILOG_SRC_DIR}/**/*.{ext}");
let pattern = format!("{dir}/**/*.{ext}");
glob::glob(&pattern)
.into_iter()
.flatten()
.filter_map(|entry| entry.ok()) // drop bad paths
.filter(|p| p.is_file())
})
.collect()
.collect();
// xrun compiles in the order it is given files and a package has to be
// compiled before whatever imports it, so `*_pkg.sv` goes first. Nothing
// else here has an ordering requirement.
files.sort_by_key(|p| {
!p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with("_pkg.sv"))
});
files
}

/// Sources that are the same at every level: the digital structure of the
/// tiles, the PHY that wires them together, and the benches written against the
/// model contract.
pub fn common_src_files() -> Vec<PathBuf> {
sources_under(COMMON_DIR)
}

/// The analog models of one level.
pub fn model_src_files(level: Level) -> Vec<PathBuf> {
sources_under(level.dir())
}

/// Everything needed to simulate the PHY at `level`.
pub fn get_src_files(level: Level) -> Vec<PathBuf> {
let mut files = common_src_files();
files.extend(model_src_files(level));
files
}

pub fn simulate(
Expand Down Expand Up @@ -103,3 +170,39 @@ pub fn simulate(
}
Ok(())
}

/// Shared support for the per-module `tests` mods: both levels of a bench are
/// run the same way, so the running and the checking live here rather than
/// being written out once per module.
#[cfg(test)]
pub(crate) mod tests {
use std::fs::read_to_string;

use anyhow::Result;

use super::{Level, get_src_files, simulate};
use crate::tests::out_dir;

/// Runs `tb` against `level`'s models and returns everything xrun printed.
///
/// The work directory carries the level, so the two levels of one bench do
/// not overwrite each other's logs.
pub fn run(level: Level, tb: &str) -> Result<String> {
let work_dir = out_dir(format!("{}_{}", tb, level.dir_name()));
simulate(get_src_files(level), tb, &work_dir)?;
Ok(read_to_string(work_dir.join("xrun.out"))?)
}

/// Runs `tb` and fails if it printed anything the benches use to report a
/// mismatch.
pub fn expect_clean(level: Level, tb: &str) -> Result<()> {
let output = run(level, tb)?;
assert_eq!(
output.matches("Error").count(),
0,
"{tb} at the {} level should have no functionality errors",
level.dir_name()
);
Ok(())
}
}
29 changes: 16 additions & 13 deletions rs/src/verilog/phy.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
use const_format::concatcp;

use crate::verilog::VERILOG_SRC_DIR;

pub const PHY_SRC: &str = concatcp!(VERILOG_SRC_DIR, "/phy.sv");
//! The whole PHY: every lane's tiles, the clock distribution between them, and
//! a loopback from each transmitter to the receiver facing it.
//!
//! Run at every level, which is what says the levels are interchangeable at
//! more than cell granularity. The circuit level is by far the longest bench
//! here -- the switched-capacitor front ends put the analog solver on
//! femtosecond steps, twenty lanes over -- so reach for the eye level while
//! iterating.

#[cfg(test)]
mod tests {
use anyhow::Result;
use test_log::test;

use crate::{
tests::out_dir,
verilog::{get_src_files, simulate},
};
use crate::verilog::{Level, tests::expect_clean};

#[test]
fn eye() -> Result<()> {
expect_clean(Level::Eye, "phy_tb")
}

#[test]
fn phy() -> Result<()> {
let work_dir = out_dir("phy");
simulate(get_src_files(), "phy_tb", &work_dir)?;
Ok(())
fn circuit() -> Result<()> {
expect_clean(Level::Circuit, "phy_tb")
}
}
Loading
Loading