Verilog Quickstart

note

This tutorial is aimed at Unix-like systems like macOS, Linux, and WSL.

In this tutorial, we'll setup a SystemVerilog project and test our code with Marlin. You can find the full source code for this tutorial here (see in particular the simple_test.rs file). We won't touch on the advanced aspects or features; the goal is just to provide a simple overfiew sufficient to get started.

Part 1: The Basics

Let's call our project "tutorial-project" (you are free to call it however you like):

mkdir tutorial-project
cd tutorial-project
git init # optional, if using git

Here's what our project will look like in the end:

.
├── Cargo.toml
├── Cargo.lock
├── .gitignore
├── src
│   ├── lib.rs
│   ├── main.sv
└── tests
    └── simple_test.rs

We'll write a very simple SystemVerilog module: one that forwards its inputs to its outputs.

mkdir src
vi src/main.sv

I'm using the vi editor here, but you can use whichever editor you prefer.

For our forwarding module, we'll just pass a medium-sized input to a corresponding output:

// file: src/main.sv
module main(
    input[31:0] medium_input,
    output[31:0] medium_output
);
    assign medium_output = medium_input;
endmodule

Part 2: Testing

Now that we have the setup out of the way, we can start testing our code from Rust. We'll initialize a Rust project:

cargo init --lib

In the Cargo.toml generated, we'll want to add some dependencies:

cargo add marlin --features verilog
cargo add snafu --dev

The only required crate is marlin, but I strongly recommend at this stage of development to use snafu, which will display a human-readable error trace upon Result::Err.

caution

Please use snafu! 😂

In the lib.rs, we'll create the binding to our Verilog module:

#![allow(unused)]
fn main() {
// file: src/lib.rs
use marlin::verilog::prelude::*;

#[verilog(src = "src/main.sv", name = "main")]
pub struct Main;
}

This tells Marlin that the struct Main should be linked to the main module in our Verilog file.

help

It's not necessary to even use the lib.rs -- you can put marlin in your [dev-dependencies] section in Cargo.toml and construct the bindings directly in your test files.

Finally, we'll want to actually write the code that drives our project in simple_test.rs:

mkdir tests
vi tests/simple_test.rs
#![allow(unused)]
fn main() {
// file: tests/simple_test.rs
use tutorial_project::Main;
use marlin::verilator::{VerilatorRuntime, VerilatorRuntimeOptions};
use snafu::Whatever;

#[test]
//#[snafu::report]
fn forwards_u32max_correctly() -> Result<(), Whatever> {
    let runtime = VerilatorRuntime::new(
        "build".into(),
        &["src/main.sv".as_ref()],
        &[],
        [],
        VerilatorRuntimeOptions::default(),
    )?;

    let mut main = runtime.create_model_simple::<Main>()?;

    main.medium_input = u32::MAX;
    println!("{}", main.medium_output);
    assert_eq!(main.medium_output, 0);
    main.eval();
    println!("{}", main.medium_output);
    assert_eq!(main.medium_output, u32::MAX);

    Ok(())
}
}

caution

Using #[snafu::report] on the function gives error messages that are actually useful, but sometimes breaks LSP services like code completion. I recommend to only apply it to your test functions when you actually encounter an error.

Let's break down the relevant parts of what's going on here.

We first setup the Verilator runtime configuration. We'll use a build directory called "build" in the local directory.

#![allow(unused)]
fn main() {
let runtime = VerilatorRuntime::new(
    "build".into(),                             // build directory (relative)
    &["src/main.sv".as_ref()],                  // source files
    &[],                                        // include search paths
    [],                                         // DPI functions
    VerilatorRuntimeOptions::default_logging(), // configuration
)?;
}

tip

Add this build directory to your .gitignore file if you're using git.

You can fill in the source files (2nd argument) by, for example, finding all .v files in a source direcory with std::fs::read_dir. Since we only have one, we've hardcoded it.

Then, we instantiate the model:

#![allow(unused)]
fn main() {
let mut main = runtime.create_model::<Main>()?;
}

I won't comment on the rest; it's just regular Rust --- including the part where we assign to values and call eval() on the model object! (Yes, that is the same as Verilator's evaluation method).

Finally, we can simply use cargo test to drive our design!