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)