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.
- 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. - You must annotate that black box declaration in such a way that
yosys
knows the module is externally defined. - 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_driver", &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: String
The Verilog code to create the black box in your firmware
name: String
The name of the black box IP module.