Spade Quickstart

note

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

In this tutorial, we'll setup a Spade project and test our code with Marlin. You can find the full source code for this tutorial here.

I'll be assuming you've read the tutorial on testing Verilog projects; if not, read that first and come back.

Also, make sure you have a Spade toolchain installed, although we'll only be using the Swim build tool (follow the instructions here to install it).

note

If you already have a Swim project and are looking to integrate Marlin into it, you don't need to read Part 1 too carefully.

Part 1: Making a Swim Project

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

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

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

.
├── swim.toml
├── swim.lock
├── Cargo.toml
├── src
│   ├── lib.rs
│   └── main.spade
└── tests
    └── simple_test.rs

In main.spade (which should already exist after running swim init), we'll write some simple Spade code:

// file: src/main.spade
#[no_mangle(all)]
entity main(out: inv &int<8>) {
    set out = &42;
}

You can read the Spade book for an introduction to Spade; this tutorial will not focus on teaching the language. Nonetheless, the essence of the above code is to expose an inverted wire which we pin to the value 42 (think of assigning to an output in Verilog). We'll write a very simple SystemVerilog module: one that forwards its inputs to its outputs.

Part 2: Setting up Marlin

cargo init --lib
cargo add marlin --features spade --dev
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 test file, we'll create the binding to our Spade module:

mkdir tests
vi tests/simple_test.rs
#![allow(unused)]
fn main() {
// file: tests/simple_test.rs
use marlin::spade::prelude::*;

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

This tells Marlin that the struct Main should be linked to the main entity in our Spade file. You can instead put this in your lib.rs file if you prefer.

Finally, we'll want to actually write the code that drives our hardware:

// file: tests/simple_test.rs
use marlin::spade::prelude::*;
use snafu::Whatever;

#[test]
//#[snafu::report]
fn main() -> Result<(), Whatever> {
    let runtime = SpadeRuntime::new(SpadeRuntimeOptions {
        call_swim_build: true, /* warning: not thread safe! don't use if you
                                 * have multiple tests */
        ..Default::default()
    })?;

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

    main.eval();
    println!("{}", main.out);
    assert_eq!(main.out, 42); // hardcoded into Spade source

    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.

Finally, we can simply use cargo test to drive our design! It will take a while before it starts doing Marlin dynamic compilation because it needs to first build the Spade project by invoking the Spade compiler.

warning

By default, SpadeRuntime does not swim build because it is not thread-safe. If you have multiple tests, run swim build beforehand and then run cargo test if you have disabled the automatic swim build via SpadeRuntimeOptions::call_swim_build.

Note that, unlike the Verilog project tutorial, you don't need to add another directory to your .gitignore, if you have one, because the SpadeRuntime reuses the existing build/ directory managed by Swim. Thus, you should add that to your .gitignore instead. swim init should do that automatically, though.