Crate cmtrs

Source
Expand description

cmtrs is an embedded domain-specific language in Rust for digital chip design.

cmtrs provides:

  • Rule-based RTL hardware description
  • Port connections with methods
  • Multi-cycle cycle-accurate high level control statements to simplify finite state machines (FSMs)
  • Embedded hardware generators for complex parameterization

§cmtrs Basics

§VERY IMPORTANT!!!

cmtrs use nightly features for span support. You need add a file .cargo/config.toml with the following content:

[build]
rustflags = "--cfg procmacro2_semver_exempt"

as well as a rust-toolchain.toml file with the following content:

[toolchain]
channel = "nightly-2024-12-25"

§Interface declaration

An interface is how a module looks like from the outside, which describes its type parameters, IO ports and methods.

Modules with the same interface will have the same Rust type in cmtrs, which means different implementation can be selected by during generation. From this point of view, interfaces are similar to traits.

Module methods will become real Rust methods after instantiation, which can be invoked to trigger certain behavior of the module. IO ports will be automatically connected according to method invocations.

§Example

use cmtrs::*;
itfc_declare!(
  param T; // Type parameter
  // IO declaration
  struct Counter { // Name of the interface
    set_val: input param T, // input port
    count: output param T,  // output port
  }
  // methods declaration
  method read() -> (count);  // output to 'count' port
  method set(set_val) ;      // input from 'set_val' port
);

§Module generation

To implement a module of certain interface, one must write a generator function, which generates hardware through function execution.

A generator function is a function with #[module] attribute and returns a module interface. The #[module] attribute is a proc-macro that will record all hardware behaviors inside of the function to produce a module.

§Example


#[module]
fn make_counter(lim: usize, ty: &Type) -> Counter {
  // Provide type parameters and create ios.
  let io = io! {
    T: ty
  };

  // Instantiate a register sub-module
  // `mut` here is required for the %= operator, though it is not mutable in-fact
  // `stl::reg` is the generator function of reg
  let mut reg_i = instance!(stl::reg(ty));

  // an always rule will be automatically fired if the conditions are met
  // increase reg_i if i < lim
  let inc = always! {
    [reg_i.lt(lim.lit(ty))] // guard expression
    () { // ios
      reg_i %= &reg_i + 1.lit(ty); // body
      // equals reg_i.write(reg_i.read() + 1.lit(ty))
    }
  };

  // reset reg_i to 0 if i>=lim
  let rst = always! {
    [reg_i.ge(lim.lit(ty))]
    () {
      reg_i %= 0.lit(ty);
    }
  };

  // a method will be fired if it is invoke and conditions are met
  // set reg_i to the input `set_val`
  let set = method!(
    (io.set_val) {
      reg_i %= io.set_val;
    }
  );

  // read the value of reg_i to the output `count`
  let read = method! {
    () -> (io.count) {
      ret!(&reg_i)
    }
  };

  // `set` method has confict with itself,
  // so only one `set` can be called in one cycle
  method_rel!(set C set);
    
  // combinational order of the rules and methods in one cycle
  // rules
  schedule!(read, set, inc, rst);

  // Automatically returns the generated module
  // Do not try to return anything here
}

§List of commonly used macros in module generation

  • itfc_declare! Declares an interface.
  • #[module] Make a module generator.
  • io! Provides type parameters and sub-interface instances to the module, returns a struct containing all ios and sub-interfaces.
  • anno! Give annotations to the module.
  • always! Declare a always rule.
  • method! Declare a method rule.
  • ext_method! Declare a external method.
  • method_rel! Declare the relationship between rules and methods.
  • schedule! Declare a timing order for the rules and methods within one cycle.
  • var! Make a single assign multiple use variable (wire) that can be used within a rule/method.
  • if_! Combinational condition.
  • ret! Return a value to statements at higher-level.
  • statement! Make an expression into a statement.
  • #[gen_fn] Make a generate function, which records macros calls and can be transferred back to its caller.
  • generate! Actually generate the things into the module returned by a generate function.
  • move_! Use on move || closures so that macros can be called inside of it.
  • sim_exit! Declare the finish of simulation.
  • sim_print! Print values during simulation.

§High-level control primitives

High level control primitives describes hardware behaviors that takes multiple cycles to finish. Currently we provide a sub-set of C-like primitives.

  • step! Group operations into an atomic step controlled by FSMs.
  • seq! Execute children statements in sequential cycles.
  • par! Execute children statements in parallel, then join them.
  • branch! Make a fork on the FSM, decide which statement to execute in the next cycle.
  • for_! Execute statements in C-like for loop.

High-level primitives can only be used in rules with fsm or pipeline keyword. Such rule will become stateful and will be executed through multiple cycles once fired. fsm rules are similar to a process, which takes inputs, does computation in the next cycles, and returns the result. pipeline rules are similar to coroutines, which can be repeatedly invoked for multiple inputs and outputs. Each statement at top-level in a pipeline works concurrently, and will be fired once the previous stage is finish.

§Example

The Collatz Conjucture is a sequence, where a[i+1] = a[i]/2 if a[i] is even, otherwise a[i+1]=a[i]*3+1. The following example wil calculate such sequence.

use cmtrs::*;

itfc_declare! {
  struct Collatz {
    in_: input Type::UInt(8),
    out: output Type::UInt(8)
  }
  method out () -> (out);
  method start (in_);
}

#[module]
fn make_collatz() -> Collatz {
  let io = io! {};

  let t = Type::UInt(8);
  let r = instance!(stl::reg(&t));

  let out = method!(
    () -> (io.out) { ret!(r.read()) }
  );

  let start = method!(
    fsm; // the fsm keyword
    (io.in_) {
      for_!{(r.write(io.in_);true; ; r.read().gt(literal(1, &t))) {
        branch!{r.read() & literal(1, &t) { // odd
          branch!{r.read().ne(1.lit(&t)){ // not one
            seq!{
              step!{ r.write(r.read() * literal(3, &t)); };
              step!{ r.write(r.read() + literal(1, &t)); };
            }
          } }
        } else { // even
          step!{ r.write(r.read() >> 1); };
        }
        };
      } }
    }
  );
}

§Nested Interface

An interface can be part of the interface of the parent module. This is useful for modules like FIFO or memory. Within the module generator, nested interfaces can be accessed from io!{}. From the outside, nested interfaces are public members of the Rust type of the module instance.

§Example

The sequence merge example merges two sequence from FIFOs, and put them into the output FIFO.

use cmtrs::*;

itfc_declare! {
  param T;
  struct SeqMerge {
    // a is a nested itfc, of type FIFO,
    // with parameter T, equal to the parameter T of SeqMerge
    a: itfc stl::FIFO{T: param T},
    b: itfc stl::FIFO{T: param T},
    c: itfc stl::FIFO{T: param T},
  }
}

#[module]
fn seq_merge(t: &Type) -> SeqMerge {
  let io = io! {
    T: t,
    // use io macro to provide the exact instances to the nested itfc
    a: stl::fifo_default(4, t),
    b: stl::fifo_default(4, t),
    c: stl::fifo_default(4, t)
  };
  anno!("synthesis":"true");

  let reg_a = instance!(stl::Reg::new(t));
  let reg_b = instance!(stl::Reg::new(t));

  let reg_a_valid = instance!(stl::Reg::new(&Type::UInt(1)));
  let reg_b_valid = instance!(stl::Reg::new(&Type::UInt(1)));

  let in_a = always!(
    [!reg_a_valid.read()]
    () {
      // use nested itfc from the io
      reg_a.write(io.a.deq());
      reg_a_valid.write(true);
    }
  );
  let in_b = always!(
    [!reg_b_valid.read()]
    () {
      reg_b.write(io.b.deq());
      reg_b_valid.write(true);
    }
  );

  let out = always! {
    [reg_a_valid.read() & reg_b_valid.read() & !io.c.full()]
    () {
       let a = var!(reg_a.read());
       let b = var!(reg_b.read());
       if_!{(&a).le(&b) {
         io.c.enq(a);
         reg_a_valid.write(false);
       } else {
         io.c.enq(b);
         reg_b_valid.write(false);
       } }
     }
  };

  schedule!(out, in_a, in_b);
}

itfc_declare!(
  param T;
  struct Top {
    in_a: input param T,
    in_b: input param T,
    out_c: output param T,
  };
  method input_a(in_a);
  method input_b(in_b);
  method output_c()->(out_c);
);

#[module]
fn make_top(t: &Type) -> Top {
  let io = io! {T: t};
  anno!("synthesis":"true");

  let merger = instance!(seq_merge(t));

  let input_a = method! {
    [!merger.a.full()]
    (io.in_a) {
      // use nested itfc from the outside
      merger.a.enq(io.in_a);
    }
  };

  let input_b = method! {
    [!merger.b.full()]
    (io.in_b) {
      merger.b.enq(io.in_b);
    }
  };

  let output_c = method! {
    () -> (io.out_c) {
      merger.c.deq()
    }
  };

  schedule!(input_a, input_b, output_c);
}

§Macros for dynamic generation

Module can be completely dynamically generated, with arbitary IOs and methods. The following macros are helpful for this purpose.

§Type System

Currently cmtrs use completely dynamic types. See Type for more information.

§Operators

Currenly cmtrs support a limitted range of operators. See ops for more information.

§Standard library

cmtrs provide some standard templates in hardware, including Wire, Reg, FIFO and memories. See stl for more information.

§Elaboration and Simumlation

Elaboration and Simulation depends on crate [cmtc]. cmtrs can be elaborated into FIRRTL or System Verilog. We also support writing testbenches in cmtrs, which will be transformed into C testbench for Verilator or Khronos.

§Elaboration

use cmtrs::*;
use cmtc::*;
// To System Verilog
let module = make_module();
elaborate(module, sv_config("path/to/sv.sv")).unwrap();
// To FIRRTL
let module = make_module();
elaborate(module, fir_config("path/to/fir.fir")).unwrap();

§Testbench

A cmtrs testbench is a non-synthesizable top-module. It can use anything in normal modules, and additonally can use simulation only functionalities like Interger and sim_print.

use cmtrs::*;
use cmtc::*;


itfc_declare!(
  struct Tb {}
);

#[module]
fn make_tb() -> Tb {
  io! {}
  anno!("is_tb": "true");

  let counter = instance!(make_counter(10, &Type::UInt(4)));
  let mut cycle = instance!(stl::integer());

  let sim = always!(
    [cycle.lt(stl::int(20))]
    () {
      cycle %= &cycle + stl::int(1);
      sim_print!("cycle: ", cycle, "count: ", counter.read());
    }
  );

  let exit = always!(
    [cycle.ge(stl::int(20))]
    () {
      sim_exit!();
    }
  );

  method_rel!(sim CF exit);
}

// Make Verilator workspace
let tb = make_tb();
elaborate(tb, verilator_config("path/to/dir")).unwrap();

// Make Khronos workspace
let tb = make_tb();
elaborate(tb, ksim_config("path/to/dir")).unwrap();

The generated Veriloator workspace is configured with CMake. The executable is named Vsim.

cd path/to/dir
mkdir build
cd build
cmake -GNinja ..
ninja
./Vsim

The generated Khonos workspace is configured with MakeFile. The executable have the same name as the top module.

cd path/to/dir
make all
./name_of_top_module

Re-exports§

pub use type_sys::Type;
pub use interface::*;

Modules§

gen_ir
interface
Cmtrs interface for both generators and users
stl
Standard library for Cmtrs
type_sys
Cmtrs Type System
utils

Macros§

always
let #rule_name = always! { [fsm; | pipeline;] [\[#guard\]] (#inputs) [-> (#outputs)] { #body } };
anno
anno!("key" : "value", *);
branch
branch selectively executes statements according to a condition. In fact, branch actually makes branchs on a FSM. Different from if_!, it can have sequential control statements within its body, but itself can not be inside of a step!.
distinguisher
distinguisher!(#distinguish_str)
ext_method
let #method_name = ext_method!{ #enable; #ready; [\[#guard\]] (#inputs) [-> (#outputs)] { #body } };
external
external!([inputs_names], [output_names], Option<clock_name>, Option<reset_name>, firrtl_string);
for_
for_ mimics a C for-loop. There are two types:
generate
generate!(#invoke_gen_fn)
if_
if_!{#cond {#then_body} [else {#else_body}]}
input
input!(#name, #type)
instance
let #instance_name = instance!(#module);
io
let io = io! { (#param: #harware_type),* (#nested_itfc: #instance),* };
itfc_declare
Declare an module interface. Type parameters, IO ports, nested interfaces and method signatures can be declared.
method
let #method_name = method! { [fsm; | pipeline;] [\[#guard\]] (#inputs) [-> (#outputs)] { #body } };
method_rel
method_rel!(#method1 C #method2), conflicts.
method_rel_raw
method_rel_raw!(#methods1 #rel #methods2). #methods is &[RuleHandle]
move_
move_!(|...| {...})
named_always
let rule = named_always! { #name; [\[#guard\]] (#inputs) [-> (#outputs)] { #body } };
named_ext_method
let method = ext_method!{ #name; #enable; #ready; [[#guard\]] (#inputs) [-> (#outputs)] { #body } };
named_instance
named_instance(#instance_name; module);
named_method
let method = named_method! { #name; [\[#guard\]] (#inputs) [-> (#outputs)] { #body } };
named_var
named_var!(#name; #value)
output
output!(#name, #type)
par
par!(#statements;*)
ret
ret!(#value);
ret_raw
ret_raw!(#values)
schedule
schedule!(#rule_or_method, *)
schedule_raw
schedule_raw!(#rules_or_methods), accepts &[RuleHandle]
seq
seq!(#statements;*)
set_name
set_name!(#name)
sim_exit
sim_exit!();
sim_print
sim_print!(#args...)
statement
statement!(#value)
step
step!(#statements;*)
var
var!(#value)

Structs§

Location
A struct containing information about the location of a panic.
MySpan
A span with file, start line, start column, end line, end column, start byte, and end byte.

Enums§

MethodRel
PrintItem
items for printing during simulation either “string” or “value”
RuleSignature
Rule signature in cmtir. always rules have inputs and outputs method rules have inputs, outputs, and side effect
RuleTiming
Rule timing in cmtir. Currently, only single-cycle/FSM/pipeline rules are supported. multi-cycle rules are not supported yet.

Traits§

IRDump

Functions§

extract_span_from_location

Attribute Macros§

gen_fn
#[gen_fn] is an attribute on functions or methods. Attributed function will act as if it is part of a module. Used together with generate! macro.
module
Implement a module generator for certain interface.