Struct rust_hdl::prelude::Wrapper

source ·
pub struct Wrapper {
    pub code: String,
    pub cores: String,
}
Expand description

The Wrapper struct provides a more convenient and flexible way to wrap external IP cores than BlackBox.

While you can wrap IP cores with BlackBox, it has some limitations. There are two significant limits to using BlackBox to wrap IP cores, and Wrapper tries to fix them both.

  • If your IP cores are parametric (for example, they take a parameter to determine an address or bitwidth), you must give them unique names to avoid problems with your toolchain.
  • You cannot rename or otherwise change any of the signal names going into the IP core when you use BlackBox.

Using Wrapper addresses both problems. To address the first problem, RustHDL (when using Wrapper), creates a wrapper module that hides the wrapped core from the global scope. This additional level of scoping allows you to parameterize/customize the external IP core, without causing conflicts at the global scope. The second problem is addressed also, since the Wrapper struct allows you to write Verilog “glue code” to either simplify or otherwise fix up the interface between the IP core and the RustHDL firmware around it.

To use the Wrapper you must provide a custom implementation of the [hdl] function in the [Logic] trait. The Wrapper variant has two members. The first member is the [code] where you can write the Verilog glue code to instantiate the IP core, parameterize it, and connect it to the inputs and outputs of the RustHDL object. The second member is the [cores] member, where you provide whatever blackbox code is required for the toolchain to accept your verilog. This typically varies by toolchain. To get yosys to accept the verilog, you will need to provide (* blackbox *) attributes and module definitions for each external IP core.

Let’s look at some examples.

Examples

In the BlackBox case, we looked at wrapping a clock buffer into an IP core. Let’s redo the same exercise, but with slightly better ergonomics. Here is the definition of the IP core provided by the FPGA vendor

module IBUFDS(I, B, O);
   input I;
   input B;
   output O;
endmodule

This core is very simple, but we will try and improve the ergonomics of it, and add a simulation model.

pub struct ClockDriver {
   pub clock_p: Signal<In, Clock>,
   pub clock_n: Signal<In, Clock>,
   pub sys_clock: Signal<Out, Clock>,
}

This time, our ClockDriver can use reasonable signal names, because we will use the glue layer to connect it to the IP core. That glue layer is very helpful for remapping signals, combining them or assigning constant values.

We will also add a simulation model this time, to demonstrate how to do that for an external core.

As in the case of BlackBox, we will use the [LogicBlock] derive macro to add the [Logic] trait to our circuit (so RustHDL can work with it), and the Default trait as well, to make it easy to use. The [Logic] trait for this circuit will need to be implemented by hand.


impl Logic for ClockDriver {
    fn update(&mut self) {
        todo!()
    }

    fn connect(&mut self) {
        todo!()
    }

    fn hdl(&self) -> Verilog {
        todo!()
    }
}

The [Logic] trait requires 3 methods [Logic::update], [Logic::connect], and [Logic::hdl]. The [Logic::update] method is used for simulation, and at the moment, black box modules are not simulatable. So we can accept the default implementation of this. The [Logic::connect] method is used to indicate which members of the circuit are driven by the circuit. A better name might have been drive, instead of connect, but we will stick with the current terminology. You can think of it in terms of an integrated circuit - outputs, are generally driven and are internally connected, while inputs are generally driven from outside and are externally connected.

We also want to create a simulation model for our IP core. This is how RustHDL will know how to include the behavior of the core when it is integrated into simulations. You can skip this step, of course, but then your black box IP cores will be pretty useless for simulation purposes.

A double-to-single ended clock driver is a fairly complicated piece of analog circuitry. It normally sends a clock edge when the positive and negative going clocks cross. For well behaved differential clocks (which is likely the case in simulation), this amounts to just buffering the positive clock, and ignoring the negative clock. We will need to build a simulation model that includes enough detail to make it useful, but obviously, the fidelity will be limited. For this example, we will opt to simply ignore the negative going clock, and forwarding the positive going clock (not a good idea in practice, but for simulations it’s fine).


impl Logic for ClockDriver {
    fn update(&mut self) {
        self.sys_clock.next = self.clock_p.val();
    }

    fn connect(&mut self) {
        self.sys_clock.connect();
    }

    fn hdl(&self) -> Verilog {
        todo!()
    }
}

Now, we need an implementation for the HDL for this Clock driver. That is where we need the Wrapper struct.


impl Logic for ClockDriver {
    fn update(&mut self) {
        self.sys_clock.next = self.clock_p.val();
    }

    fn connect(&mut self) {
        self.sys_clock.connect();
    }

     fn hdl(&self) -> Verilog {
        Verilog::Wrapper(Wrapper {
          code: r#"
    // We can remap the names here
    IBUFDS ibufds_inst(.I(clock_p), .B(clock_n), .O(sys_clock));

"#.into(),
          cores: r#"
(* blackbox *)
module IBUFDS(I, B, O)
  input I;
  input B;
  output O;
endmodule"#.into(),
        })
     }
}

With all 3 of the methods implemented, we can now create an instance of our clock driver, synthesize it, and test it. Here is the completed example:


#[derive(LogicBlock, Default)]
pub struct ClockDriver {
  pub clock_p: Signal<In, Clock>,
  pub clock_n: Signal<In, Clock>,
  pub sys_clock: Signal<Out, Clock>,
}

impl Logic for ClockDriver {
    fn update(&mut self) {
        self.sys_clock.next = self.clock_p.val();
    }

    fn connect(&mut self) {
        self.sys_clock.connect();
    }

     fn hdl(&self) -> Verilog {
        Verilog::Wrapper(Wrapper {
          code: r#"
    // This is basically arbitrary Verilog code that lives inside
    // a scoped module generated by RustHDL.  Whatever IP cores you
    // use here must have accompanying core declarations in the
    // cores string, or they will fail verification.
    //
    // In this simple case, we remap the names here
    IBUFDS ibufds_inst(.I(clock_p), .B(clock_n), .O(sys_clock));

"#.into(),
          cores: r#"
(* blackbox *)
module IBUFDS(I, B, O);
  input I;
  input B;
  output O;
endmodule"#.into(),
        })
     }
}

// Let's create our ClockDriver.  No [TopWrap] is required here.
let mut x = ClockDriver::default();
x.clock_p.connect(); // Drive the positive clock from outside
x.clock_n.connect(); // Drive the negative clock from outside
x.connect_all();     // Wire up x and its internal components
let v = generate_verilog(&x);  // Generates verilog and validates it
yosys_validate("clock", &v)?;

Fields§

§code: String

The Verilog code to instantiate the black box core, and connect its inputs to the argument of the current LogicBlock kernel.

§cores: String

Blackbox core declarations needed by some synthesis tools (like yosys)

Trait Implementations§

source§

impl Clone for Wrapper

source§

fn clone(&self) -> Wrapper

Returns a copy of the value. Read more
1.0.0 · source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
source§

impl Debug for Wrapper

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

source§

impl<T> Any for Twhere T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for Twhere T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for Twhere T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for Twhere U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Pointable for T

§

const ALIGN: usize = mem::align_of::<T>()

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
source§

impl<T> ToOwned for Twhere T: Clone,

§

type Owned = T

The resulting type after obtaining ownership.
source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
source§

impl<T, U> TryFrom<U> for Twhere U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for Twhere U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for Twhere V: MultiLane<T>,

§

fn vzip(self) -> V