pub struct BlackBox {
    pub code: String,
    pub name: String,
}
Expand description

The BlackBox struct provides a way to wrap a blackbox, externally provided IP core.

You will frequently in the world of FPGA firmware need to wrap an external IP core that provides some functionality you cannot implement yourself. For example, an analog clock driver circuit that takes a double ended clock and converts it to a single ended clock. Your synthesis tools may be smart enough to infer such a device, but most likely they will need help. That is where a black box references come in. Think of it as a link reference to an external library. Assuming you are going to validate the code using yosys, you will need to satisfy a few additional constraints.

  1. You must provide a black box declaration for any module that is external to your RustHDL firmware, or yosys will fail to validate the resulting firmware.
  2. You must annotate that black box declaration in such a way that yosys knows the module is externally defined.
  3. RustHDL does not currently have the ability to dynamically rename conflicting instances of black box firmware. So you can only include one instance of each black box firmware in your design. This is generally not what you want, and the Wrapper struct provides a better way to wrap a black box core (with some slight tradeoffs).

To use the BlackBox variant, you will need to provide a custom implementation of the [hdl] function in the [Logic] trait. This is fairly straightforward to do, so lets look at some examples.

Example Let’s imagine we have a module that represents a custom piece of hardware that takes a differential clock from outside the FPGA, and converts into a single ended clock to drive logic. This is clearly going to be custom analog circuitry, so it’s natural that we need to wrap an external black box primitive to get that functionality.

The BlackBox variant does not allow much flexibility. Our RustHDL struct that describes the external IP must match the underlying Verilog exactly, or the included IP will not work correctly. For the clock buffer, we have a Verilog definition from the manufacturer that looks like this

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

It’s succinct, but the argument names are pretty terse. Unfortunately, we can’t remap them or make it more ergonomic with a BlackBox. For those types of operations, we need to use a Wrapper. For now, we start with a struct to describe the circuit. Note that at least the name of the RustHDL struct does not have to be the same as the black box IP core.

pub struct ClockDriver {
   pub I: Signal<In, Clock>,
   pub B: Signal<In, Clock>,
   pub O: Signal<Out, Clock>,
}

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. For our black box, the [Logic::connect] trait implementation is very simple:


impl Logic for ClockDriver {
    fn update(&mut self) {
        // No simulation model
    }

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

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

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


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

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

     fn hdl(&self) -> Verilog {
        Verilog::Blackbox(BlackBox {
          code: r#"
(* blackbox *)
module IBUFDS(I, B, O)
  input I;
  input B;
  output O;
endmodule"#.into(),
          name: "IBUFDS".into()
        })
     }
}

Just to re-emphasize the point. Your FPGA provider will give you a Verilog declaration for the IP core. You cannot change it! The names of the signals must be the same, even if Rust complains about your intransigence.

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 I: Signal<In, Clock>,
  pub B: Signal<In, Clock>,
  pub O: Signal<Out, Clock>,
}

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

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

     fn hdl(&self) -> Verilog {
        Verilog::Blackbox(BlackBox {
          code: r#"
(* blackbox *)
module IBUFDS(I, B, O);
  input I;
  input B;
  output O;
endmodule
"#.into(),
          name: "IBUFDS".into()
        })
     }
}

// For technical reasons, the top circuit of a RustHDL firmware
// cannot be a black box.  So we use TopWrap to wrap it with one.
let mut x = TopWrap::new(ClockDriver::default());
x.uut.I.connect(); // Drive the positive clock from outside
x.uut.B.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)?;

Wrapping Parameteric IP Cores

The BlackBox variant has a couple of peculiarities that we have hidden in this example. First, note that we pass the module name back to RustHDL in the BlackBox instantiation. This is because RustHDL needs to know what the module is called so it can refer to it in the generated code.

In general RustHDL tries to avoid contention between modules with the same name by automatically namespacing them. That means that if you have a module that is used in two different places in your code, it will get two different names. This is because of the parametric nature of the generated code. RustHDL does not assume (or know) that your two modules will generate identical Verilog. So it assumes they will be different and creates two different named instances.

To see how that works, let’s create a minimum example. For test, we will use a single bit inverter.


// First a basic inverter example
#[derive(LogicBlock, Default)]
struct Inverter {
   sig_in: Signal<In, Bit>,
   sig_out: Signal<Out, Bit>,
}

// All it does is set the output signal to the inverse
// of the input signal.
impl Logic for Inverter {
   #[hdl_gen]
   fn update(&mut self) {
       self.sig_out.next = !self.sig_in.val();
   }
}

// Now we create a circuit with 2 inverters connected
// back to back.  The net result is a do-nothing (buffer?).
#[derive(LogicBlock, Default)]
struct DoubleKnot {
   sig_in: Signal<In, Bit>,
   sig_out: Signal<Out, Bit>,
   knot_1: Inverter,
   knot_2: Inverter,
}

impl Logic for DoubleKnot {
   #[hdl_gen]
   fn update(&mut self) {
      self.knot_1.sig_in.next = self.sig_in.val();
      self.knot_2.sig_in.next = self.knot_1.sig_out.val();
      self.sig_out.next = self.knot_2.sig_out.val();
   }
}

// Now, let's create a [DoubleKnot] and see what we get
let mut x = DoubleKnot::default();
// The `sig_in` input on `x` needs to be driven or connected
x.sig_in.connect();
x.connect_all();
let v = generate_verilog(&x);
// If you examine the generated code, you will see it contains
// two instances of modules, one named `top$knot_1` and the
assert!(v.contains("top$knot_1 knot_1"));
// and the second is `top$knot_2`.
assert!(v.contains("top$knot_2 knot_2"));

The problem arises when you use a BlackBox Verilog declaration. In particular, RustHDL does not wrap your declaration (the Verilog is just copied to the output), so it does not know that two different instances of the same blackbox IP may represent different things. A classic case is in the case of a parameterized blackbox IP core. In that case, it is up to you to rename the different IP cores so that they do not conflict. A better way around this is to use the Wrapper variant, since that is easier to use in most cases.

Fields

code: Stringname: String

Trait Implementations

Returns a copy of the value. Read more

Performs copy-assignment from source. Read more

Formats the value using the given formatter. Read more

Auto Trait Implementations

Blanket Implementations

Gets the TypeId of self. Read more

Immutably borrows from an owned value. Read more

Mutably borrows from an owned value. Read more

Returns the argument unchanged.

Calls U::from(self).

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

The alignment of pointer.

The type for initializers.

Initializes a with the given initializer. Read more

Dereferences the given pointer. Read more

Mutably dereferences the given pointer. Read more

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

The resulting type after obtaining ownership.

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

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

The type returned in the event of a conversion error.

Performs the conversion.

The type returned in the event of a conversion error.

Performs the conversion.