* Write FPGA Firmware using Rust! *
RustHDL is a crate that allows you to write FPGA firmware using Rust!
Specifically, rust-hdl
compiles a subset of Rust down to Verilog so that
you can synthesize firmware for your FPGA using standard tools. It also
provides tools for simulation, verification, and analysis along with strongly
typed interfaces for making sure your design works before heading to the bench.
The workflow is very similar to GPU programming. You write everything in Rust,
including an update kernel
that compiles down onto the hardware. You can simulate
and verify everything from the safety and comfort of your Rust environment, and
then head over to standard synthesis tools to get files that program your FPGA.
You may want:
The definitive example in FPGA firmware land is a simple LED blinker. This typically involves a clock that is fed to the FPGA with a pre-defined frequency, and an output signal that can control an LED. Because we don't know what FPGA we are using, we will do this in simulation first. We want a blink that is 250 msec long every second, and our clock speed is (a comically slow) 10kHz. Here is a minimal working Blinky! example:
```rust use std::time::Duration; use rusthdl::core::prelude::*; use rusthdl::docs::vcd2svg::vcdtosvg; use rust_hdl::widgets::prelude::*;
const CLOCKSPEEDHZ : u64 = 10_000;
struct Blinky {
pub clock: Signal
impl Default for Blinky { fn default() -> Self { Self { clock: Default::default(), pulser: Pulser::new(CLOCKSPEEDHZ, 1.0, Duration::from_millis(250)), led: Default::default(), } } }
impl Logic for Blinky { #[hdl_gen] fn update(&mut self) { self.pulser.clock.next = self.clock.val(); self.pulser.enable.next = true.into(); self.led.next = self.pulser.pulse.val(); } }
fn main() { let mut sim = simplesim!(Blinky, clock, CLOCKSPEEDHZ, ep, { let mut x = ep.init()?; waitclockcycles!(ep, clock, x, 4*CLOCKSPEED_HZ); ep.done(x) });
let mut uut = Blinky::default();
uut.connect_all();
sim.run_to_file(Box::new(uut), 5 * SIMULATION_TIME_ONE_SECOND, "blinky.vcd").unwrap();
vcd_to_svg("/tmp/blinky.vcd","images/blinky_all.svg",&["uut.clock", "uut.led"], 0, 4_000_000_000_000).unwrap();
vcd_to_svg("/tmp/blinky.vcd","images/blinky_pulse.svg",&["uut.clock", "uut.led"], 900_000_000_000, 1_500_000_000_000).unwrap();
} ```
Running the above (a release run is highly recommended) will generate a vcd
file (which is
a trace file for FPGAs and hardware in general). You can open this using e.g., gtkwave
.
If you have, for example, an Alchitry Cu board you can generate a bitstream for this exampling
with a single call. It's a little more involved, so we will cover that in the detailed
documentation. It will also render that vcd
file into an svg
you can view with an ordinary
web browser. This is the end result showing the entire simulation:
Here is a zoom in showing the pulse to the LED
The flow behind RustHDL is the following:
struct
s, composed of other circuit elements and
signal wires that interconnect them.#[derive(LogicBlock)]
annotation on the struct adds autogenerated code needed by
RustHDL.impl Logic
on your struct
, and provide the fn update(&mut self)
method, which
is the HDL update kernel.#[hdl_gen]
attribute to generate HDL from the Rust codeThe rest is detail. Some final things to keep in mind.
rustc
compiler must be satisfied with your
design first. That means types, exhaustive enum matching, etc.There are a couple of key types you should be comfortable with to use RustHDL. The first is the Bits type, which provides a compile-time arbitrary width bit vector.
The Bits type is a Copy
enabled type that you can construct from integers,
from the Default
trait, or from other Bits
. Mostly, it is meant to stay out of your way
and behave like a u32
.
rust
let x: Bits<50> = Default::default();
This will construct a length 50 bit vector that is initialized to all 0
.
You can also convert from literals into bit vectors using the [From] and [Into] traits,
provided the literals are of the u64
type.
rust
let x: Bits<50> = 0xBEEF.into();
In some cases, Rust complains about literals, and you may need to provide a suffix:
rust
let x: Bits<50> = 0xDEAD_BEEF_u64.into();
However, in most cases, you can leave literals suffix-free, and Rust will automatically
determine the type from the context.
You can construct a larger constant using the [bits] function. If you have a literal of up to
128 bits, it provides a functional form
rust
let x: Bits<200> = bits(0xDEAD_BEEE); // Works for up to 128 bit constants.
There is also the [ToBits] trait, which is implemented on the basic unsigned integer types. This trait allows you to handily convert from different integer values
rust
let x: Bits<10> = 32_u8.to_bits();
The Bits type supports a subset of operations that can be synthesized in hardware. You can perform
Bits
of the same size using the +
operatorBits
of the same size using the -
operatorAND
between Bits
of the same size using the &
operatorOR
between Bits
of the same size using the |
operatorXOR
(Exclusive Or) between Bits
of the same size using the ^
operatorBits
of the same size using ==
and !=
operators>,>=,<,<=
) between Bits
of the same size - these are
always treated as unsigned values for comparison purposes.<<
operatorNOT
using the !
prefix operatorThese should feel natural when using RustHDL, as expressions follow Rust's rules (and not Verilog's).
For example:
rust
let x: Bits<32> = 0xDEAD_0000_u32.to_bits();
let y: Bits<32> = 0x0000_BEEF_u32.to_bits();
let z = x + y;
assert_eq!(z, 0xDEAD_BEEF_u32.to_bits());
You can, of course, construct expressions of arbitrary complexity using parenthesis, etc. The only real surprise may be at synthesis time, when you try to fit the expression onto hardware.
Signals are software abstractions to represent physical wires. The Signal type is generic over a couple of parameters. The first is meant to indicate the driver of the wire. In RustHDL, every wire must have exactly one driver. It is the hardware equivalent of the single writer principle. You can have as many readers as you want, but only one writer. Unfortunately, there are some subtleties here, and declaring ownership of a wire using the type system is imperfect. Instead, we settle for a signalling mechanism of intent. So you mark a signal as how you intend to use it in your logic. For example, consider the following circuit:
```rust
pub struct My8BitAdder {
pub input1: Signal
In this case, the fields of the adder circuit are marked as
pubso they can be accessed from
outside the circuit. The [Direction](core::signal::Direction) argument to the [Signal] indicates
how the given circuit intends to utilize the various wires. In this case,
input1and
input2
should be considered inputs, and
outputis, obviously, an output. As such,
My8BitAdderis
promising you that it will drive the
output` wire. If it fails to actually do so (by leaving
it undriven), then you will get an error when you try to use it in a design.
RustHDL does not allow undriven nets. They are treated similarly to uninitialized memory in Rust. You must drive every net in the design. Furthermore, you can have only one driver for each net. These two principles are core to RustHDL!
The second part of a [Signal] is that it is typed. In general, the type signature is meant
to convey something about the nature of the data being stored or passed. In the case of
My8BitAdder
, it doesn't say much - only that the input is an unsigned 8-bit value. But
the types can be more complicated, including collections of signals running in multiple
directions (as is typical for a bus or other complex interface).
Signals can also be bidirectional with the InOut designation. But you typically can only use these types of signals at the edge of your device. More on that elsewhere.
The definition of [Signal] also indicates how it should be used. [Signal]'s cannot be assigned to using usual semantics. ```rust
pub struct Signal
To change (drive) the value of a signal, you assign to the next
field. To read the
value of the signal (i.e. to get it's current state without driving it), you use the val()
method.
This is in keeping with the idea that you treat the signal differently if you want to drive
it to some value, versus if you want to read it, as in hardware, these are different functions.
In most cases, you will read from val()
of the input signals, and write to the .next
of the
output signal. For example, in the My8BitAdder
example, you would read from input_1.val()
and from input_2.val()
, and write to output.next
. Like this:
```rust
pub struct My8BitAdder {
pub input1: Signal
impl Logic for My8BitAdder { fn update(&mut self) { self.output.next = self.input1.val() + self.input2.val(); } }
```
In general, this is the pattern to follow. However, there are some exceptions. Sometimes, you will want a "scratchpad" for holding intermediate results in a longer expression. For example, suppose you want to logically OR a bunch of values together, but want to logically shift them into different positions before doing so. Let us assume you have a logical block that looks like this:
```rust
pub struct OrStuff {
pub val1: Signal
In this case, the pad
field (which is private to the logic) has a direction of Local
,
which means it can be used to write and read from in the same circuit, as long as you write first!
Hence, you can do something like this in the update
method
```rust
impl Logic for OrStuff { fn update(&mut self) { self.pad.next = 0.into(); // Write first self.pad.next = self.pad.val() | bitcast::<8,1>(self.val1.val().into()); // Now we can read and write to it self.pad.next = self.pad.val() | (bitcast::<8,4>(self.val2.val()) << 1); self.pad.next = self.pad.val() | (bitcast::<8,2>(self.val3.val()) << 5); self.pad.next = self.pad.val() | (bitcast::<8,1>(self.val4.val().into()) << 7); self.combined.next = self.pad.val(); } } ```
You can understand this behavior by either "folding" all of the expressions into a single
long expression (i.e., by eliminating self.pad
altogether) and just assigning the output
to an expression consisting of the various inputs OR-ed together. Nonetheless, it is
handy to be able to compute intermediate values and read them back elsewhere in the code.
.next
should never appear on the right hand side of an expression! *The following code will fail to compile, because once we try to derive HDL from the result, RustHDL realizes it makes no sense.
```compile_fail
impl Logic for OrStuff { #[hdlgen] fn update(&mut self) { self.pad.next = 0.into(); // Write first self.pad.next = self.pad.next | bitcast::<8,1>(self.val1.val().into()); // Fails! Can only write to .next self.combined.next = self.pad.val(); } } ```
Detecting the case in which you fail to write to a signal before reading from it is more complicated and must be done a run time. The macro processor is not sophisticated enough to detect that case at the moment. However, it can be found when your logic is checked for correctness by the static analyzer.
Normally, the Verilog code generator or the Simulation engine will statically check your design for you. However, you can also check the design yourself using the check_all function. Here is an example of that check being run on a logic block that attempts to write to an input signal being passed into the block. The example panics because
```rust
struct BadActor {
pub in1: Signal
impl Logic for BadActor { #[hdl_gen] fn update(&mut self) { // This is definitely not OK self.in1.next = true; // This is fine self.out1.next = self.in2.val(); } }
// This will panic with an error of CheckError::WritesToInputs, pointing to self.in1 check_all(&BadActor::default()).unwrap() ```
There is only one trait that you typically need to implement to get things to work in RustHDL with the simulation and synthesis frameworks. That is the Logic trait. Although you will rarely (if ever) need to implement the methods themselves, here is the full definition of the trait: ```rust
pub trait Logic {
fn update(&mut self);
fn connect(&mut self) {}
fn hdl(&self) -> Verilog {
Verilog::Empty
}
fn timing(&self) -> Vec
The methods are quite simple:
update
- this updates the state of the logical block based on the inputs and internal state.
In general, this is where the action of the logical block takes place.connect
- this is where we claim whatever signals we drive, by calling connect
on them.hdl
- this method returns the Verilog description for our logical block in the form of
an Verilog enum.timing
- this is where specific timing exceptions or requirements are expressed for the
logical block.In almost all cases, you will use the #[derive(LogicBlock)]
macro to derive all of the traits from
your own update
method, written in Rust. If we revisit the Blinky
example, note that
we only provided the update
method, with an attribute of #[hdl_gen]
, which in turn
generated the remaining trait implementations:
```rust
struct Blinky {
pub clock: Signal
impl Logic for Blinky { #[hdl_gen] fn update(&mut self) { self.pulser.clock.next = self.clock.val(); self.pulser.enable.next = true.into(); self.led.next = self.pulser.pulse.val(); } } ```
There are a couple of other traits that RustHDL uses that you should be aware of.
Synth
- this trait is provided on types that can be represented in hardware, i.e. as
a set of bits. You will probably not need to implement this trait yourself, but if you
need some special type representation Foo
, and impl Synth for Foo
, then RustHDL will
be able to generate Verilog code for it.Block
- this trait is needed on any struct
that is a composition of circuit elements
(pretty much every struct used to model a circuit). This should be auto-derived.Logic
- Sometimes, you will need to override the default implementations of the Logic
trait. In those cases, (when you are providing a custom simulation model, or wrapping a
black box Verilog routine), you will need to impl
the other methods.RustHDL uses procedural macros to define a subset of the Rust language that can be used to describe actual hardware. That subset is known as the synthesizable subset of Rust. It is quite limited because the end result is translated into Verilog and ultimately into hardware configuration for the FPGA.
#[hdl_gen]
attribute, the code
must still be accepted by rustc
! That means you must satisfy the type constraints, the
private nature of the struct fields, etc. This is one of the major benefits of RustHDL. It
takes code that is already been checked by rustc
and then converts it into HDL.So this will clearly fail to compile.
```compile_fail
struct Foo {
bar: Signal
impl Logic for Foo { #[hdl_gen] fn update(&mut self) { self.bar.next = "Oy!"; // Type issue here... } } ```
#[hdl_gen]
attribute can only be applied to a function (aka HDL Kernel) that
takes &mut self
as an argument. In almost all cases, you will write something like:```rust
struct Foo {}
impl Logic for Foo { #[hdl_gen] fn update(&mut self) { // Put your synthesizable subset of Rust here... } } ```
update
function must be a single block, consisting of statements.
Local definitions and items are not allowed in HDL kernels. The following, for example, will
fail. This is an example of valid Rust that is not allowed in an HDL kernel.```compile_fail
struct Foo {}
impl Logic for Foo { #[hdl_gen] fn update (&mut self) { // Fails because local items are not allowed in HDL kernels. let x = 32; } } ```
.next
or .next.field
if the signal is struct based.So valid assignments will be of the form self.<signal>.next = <expr>
, or for structure-valued
signals.
+
, -
, *
, &&
, ||
, ^
, &
, |
, <<
, >>
, ==
, <
, <=
, !=
, >
, >=
In general, binary operations require that both arguments are of the same type (e.g. bitwidth) or one of the
arguments will be a literal.
```ruststruct Foo {
pub sig1: Signal
impl Logic for Foo { #[hdl_gen] fn update(&mut self) { self.sig3.next = self.sig1.val() + 4; // Example of binop with a literal self.sig3.next = self.sig1.val() ^ self.sig2.val(); // Example of a binop with two bitvecs } } ```
-
and !
The -
operator is only supported for Signed
types. Otherwise, it makes no sense. If
you want to compute the 2's complement of an unsigned value, you need to do so explicitly.
The !
operator will flip all of the bits in the bitvector.if
) are supported
```ruststruct Foo {
pub sig1: Signal
impl Logic for Foo {
#[hdl_gen]
fn update(&mut self) {
self.sig2.next = 0.into(); // Latch prevention!
// Straight if
s are supported, but beware of latches!
// This if
statement would generate a latch if not for
// the unconditional assign to sig2
if self.sig1.val() {
self.sig2.next = 1.into();
}
// You can use else
clauses also
if self.sig1.val() {
self.sig2.next = 1.into();
} else {
self.sig2.next = 2.into();
}
// Nesting and chaining are also fine
if self.sig3.val() == 0 {
self.sig4.next = 3.into();
} else if self.sig3.val() == 1 {
self.sig4.next = 2.into();
} else {
self.sig4.next = 0.into(); // <- Fall through else prevents latch
}
}
}
- Literals (provided they implement the `Synth` trait) are supported. In most cases, you
can used un-suffixed literals (like `1` or `0xDEAD`) as add `.into()`.
- Function calls - RustHDL kernels support a very limited number of function calls, all of
which are ignored in HDL at the moment (they are provided to make `rustc` happy)
- `bit_cast`
- `signed_bit_cast`
- `unsigned_cast`
- `bits`
- `Bits`
- `Type::join` and `Type::link` used to link and join logical interfaces...
- Method calls - Kernels support the following limited set of method calls
- `get_bits` - extract a (fixed width) set of bits from a bit vector
- `get_bit` - extract a single bit from a bit vector
- `replace_bit` - replace a single bit in a bit vector
- `all` - true if all the bits in the bit vector are true
- `any` - true if any of the bits in the bit vector are true
- `xor` - true if the number of ones in the bit vector is odd
- `val`, `into`, `index`, `to_bits` - ignored in HDL kernels
rust
struct Foo {
pub sig1: Signal
impl Logic for Foo {
#[hdlgen]
fn update(&mut self) {
self.sig2.next = self.sig1.val().getbit(self.sigindex.val().index()); // <- Selects specified bit out of sig1
self.sig3.next = self.sig1.val().getbits::<3>(self.sig_index.val().index()); // Selects 3 bits starting at index sig_index
// Notice that here we have an output on both the left and right side of the assignment
// That is fine as long we we write to .next
before we read from .val
.
self.sig4.next = self.sig3.val().all(); // True if sig3 is all true
}
}
- Matches - Kernels support matching with literals or identifiers
Matches are used for state machines and implementing ROMs.
For now, `match` is a statement, not an expression! Maybe that will be fixed in a future
version of RustHDL, but for now, the value of the `match` is ignored.
Here is an example of a `match` for a state machine:
rust
enum State { Idle, Running, Paused, }
struct Foo {
pub start: Signal
impl Logic for Foo {
#[hdlgen]
fn update(&mut self) {
dffsetup!(self, clock, state); // <- setup the DFF
match self.state.q.val() {
State::Idle =>
if self.start.val() {
self.state.d.next = State::Running;
}
State::Running =>
if self.pause.val() {
self.state.d.next = State::Paused;
}
State::Paused =>
if !self.pause.val() {
self.state.d.next = State::Running;
}
}
if self.stop.val() {
self.state.d.next = State::Idle;
}
}
}
``
- Macros - some macros are supported in kernels
-
println- this is converted into a comment in the generated HDL
-
comment- also a comment
-
assert- converted to a comment
-
dff_setup- setup a DFF - this macro is converted into the appropriate HDL
-
clock- clock a set of components - this macro is also converted into the appropriate HDL
- Loops -
forloops are supported for code generation
- In software parlance, all
forloops are unrolled at compile time, so they must be of the form
for
```rust
// Mux from N separate signals, using A address bits
// For fun, it's also generic over the width of the
// signals being muxed. So there are 3 generics here:
// - D - the type of those signals
// - N - the number of signals being muxed
// - A - the number of address bits (check that 2^A >= N)
struct Mux
// The impl for this requires a for loop
impl
Since an example is instructive, here is the HDL kernel for a nontrivial circuit (the SPIMaster
),
annotated to demonstrate the various valid bits of syntax. It's been heavily redacted to make
it easier to read.
```rust // Note - you can use const generics in HDL definitions and kernels!
struct SPIMasterpub
members are the ones you can access from other circuits.
// These form the official interface of the circuit
pub clock: Signal
implself.register_out.clock.next = self.clock.val();
// v-- self.register_out.d.next = self.register_out.q.val();
registerout,
registerin,
state,
pointer,
);
// This macro is shorthand for self.strobe.next = self.clock.val();
clock!(self, clock, strobe);
// These are just standard assignments... Nothing too special.
// Note that .next
is on the LHS, and .val()
on the right...
self.strobe.enable.next = true;
self.wires.mclk.next = self.clockstate.q.val();
self.wires.msel.next = self.mselflop.q.val();
self.datainbound.next = self.registerin.q.val();
self.pointerm1.next = self.pointer.q.val() - 1;
// The match
is used to model state machines
match self.state.q.val() {
SPIState::Idle => {
self.busy.next = false;
self.clockstate.d.next = self.cpol.val();
if self.startsend.val() {
// Capture the outgoing data in our register
self.registerout.d.next = self.dataoutbound.val();
self.state.d.next = SPIState::Dwell; // Transition to the DWELL state
self.pointer.d.next = self.bitsoutbound.val(); // set bit pointer to number of bit to send (1 based)
self.registerin.d.next = 0.into(); // Clear out the input store register
self.mselflop.d.next = !self.csoff.val(); // Activate the chip select
self.continuedsave.d.next = self.continuedtransaction.val();
} else {
if !self.continuedsave.q.val() {
self.mselflop.d.next = self.csoff.val(); // Set the chip select signal to be "off"
}
}
self.mosiflop.d.next = self.mosioff.val(); // Set the mosi signal to be "off"
}
SPIState::Dwell => {
if self.strobe.strobe.val() {
// Dwell timeout has reached zero
self.state.d.next = SPIState::LoadBit; // Transition to the loadbit state
}
}
SPIState::LoadBit => {
// Note in this statement that to use the pointer register as a bit index
// into the register_out
DFF, we need to convert it with index()
.
if self.pointer.q.val().any() {
// We have data to send
self.mosiflop.d.next = self
.registerout
.q
.val()
.getbit(self.pointerm1.val().index()); // Fetch the corresponding bit out of the register
self.state.d.next = SPIState::MActive; // Move to the hold mclock low state
self.clockstate.d.next = self.cpol.val() ^ self.cpha.val();
} else {
self.mosiflop.d.next = self.mosioff.val(); // Set the mosi signal to be "off"
self.clockstate.d.next = self.cpol.val();
self.state.d.next = SPIState::Finish; // No data, go back to idle
}
}
SPIState::MActive => {
if self.strobe.strobe.val() {
self.state.d.next = SPIState::SampleMISO;
}
}
}
}
}
```
In keeping with Rust's strongly typed model, you can use enums (not sum types) in your HDL,
provided you derive the LogicState
trait for them. This makes your code much easier to
read and debug, and rustc
will make sure you don't do anything illegal with your
enums.
```rust
enum State { Idle, Running, Paused, } ```
Using enums for storing things like state has several advantages: - RustHDL will automatically calculate the minimum number of bits needed to store the enum in e.g., a register.
For example, we can create a Digital Flip Flop (register) of value State
from the next
example, and RustHDL will convert this into a 2 bit binary register.
```rust
enum State { Idle, Sending, Receiving, Done, }
struct Foo {
dff: DFF
Now imagine we add another state in the future to our state machine - say Pending
:
```rust
enum State { Idle, Sending, Receiving, Pending, Done, }
struct Foo {
dff: DFF
enum
-valued signals are valid at all timesThe strong type guarantees ensure you cannot assign arbitrary values to enum
valued
signals, and the namespaces ensure that there is no ambiguity in assignment. This example
won't compile, since On
without the name of the enum
means nothing, and State1
and
State2
are separate types. They cannot be assigned to one another.
```compile_fail
enum State1 { On, Off, }
enum State2 { Off, On, }
struct Foo {
pub sigin: Signal
impl Logic for Foo { #[hdlgen] fn update(&mut self) { self.sigout.next = On; // << This won't work either. self.sigout.next = self.sigin.val(); // << Won't compile } } ```
If for some reason, you needed to translate between enums, use a match
:
```rust
#[hdlgen] fn update(&mut self) { self.consumer.datatofifo.next = self.producer.datato_fifo.val(); self.consumer.write.next = self.producer.write.val(); self.producer.full.next = self.consumer.full.val(); self.producer.overflow.next = self.consumer.overflow.val(); } } ```
This is basically boilerplate at this point, and typing that in and getting it right
is error prone and tedious. Fortunately, RustHDL can help! RustHDL includes the
concept of an Interface
, which is basically a bus. An Interface
is generally a
pair of structs that contain signals of complementary directions and a #[derive]
macro that autogenerates a bunch of boilerplate. To continue on with our previous
example, we could define a pair of struct
s for the write interface of the FIFO
```rust
struct MyFIFOWriteReceiver {
pub datatofifo: Signal
struct MyFIFOWriteSender {
pub datatofifo: Signal
A
must have a matching named field in struct B
join
attribute tells the compiler which interface to mate to this one.So what can we do with our shiny new interfaces? Plenty of stuff. First, lets rewrite our FIFO circuit and data producer to use our new interfaces.
```rust
struct MyFIFO {
// The write interface to the FIFO - now only one line!
pub writebus: MyFIFOWriteReceiver,
// The read interface to the FIFO - unchanged and verbose!
pub datafrom_fifo: Signal
struct DataWidget { pub data_out: MyFIFOWriteSender, } ```
That is significantly less verbose! With a similar pair for MyFIFOReadSender/MyFIFOReadReceiver
you could get to something even shorter. But leaving it as is for now, what happens to our
impl Logic for Foo
? Well, RustHDL autogenerates 2 methods for each LogicInterface
. The first
one is called join
. And it, well, joins the interfaces.
rust
impl Logic for Foo {
#[hdl_gen]
fn update(&mut self) {
// Excess verbosity eliminated!!
MyFIFOWriteSender::join(&mut self.producer.data_out, &mut self.consumer.write_bus);
}
}
This is exactly equivalent to our previous 4 lines of hand crafted code, but is now automatically
generated and synthesizable. But wait! There is more. RustHDL also generates a link
method, which allows you to forward a bus from one point to another. If you think in terms
gendered cables, a join
is a cable with a Male connector on one end and a Female connector
on the other. A link
is a cable that is either Male to Male or Female to Female. Links
are useful when you want to forward an interface to an interior component of a circuit, but
hide that interior component from the outside world. For example, lets suppose that
DataWidget
doesn't actually produce the 16-bit samples. Instead, some other FPGA component
or circuit generates the 16-bit samples, and DataWidget
just wraps it along with some
other control logic. So in fact, our DataWidget
has an internal representation that looks
like this
```rust
struct DataWidget {
pub dataout: MyFIFOWriteSender,
secretguy: CryptoGenerator,
running: DFF
struct CryptoGenerator { pub data_out: MyFIFOWriteSender, // secret stuff! } ```
In this example, the DataWidget
wants to present the outside world that it is a MyFIFOWriteSender
interface, and that it can produce 16-bit data values. But the real work is being done internally
by the secret_guy
. The manual way to do this would be to connect up the signals manually. Again,
paying attention to which signal is an input (for DataWidget
), and which is an output.
rust
impl Logic for DataWidget {
#[hdl_gen]
fn update(&mut self) {
// Yawn...
self.data_out.data_to_fifo.next = self.secret_guy.data_out.data_to_fifo.val();
self.data_out.write.next = self.secret_guy.data_out.write.val();
self.secret_guy.data_out.full.next = self.data_out.full.val();
self.secret_guy.data_out.overflow.next = self.data_out.overflow.val();
}
}
In these instances, you can use the link
method instead. The syntax is
Interface::link(&mut self.outside, &mut self.inside)
, where outside
is the
side of the interface going out of the circuit, and inside
is the side of the interface
inside of the circuit. Hence, our interface can be forwarded
or linked
with a single line
like so:
rust
impl Logic for DataWidget {
#[hdl_gen]
fn update(&mut self) {
// Tada!
MyFIFOWriteSender::link(&mut self.data_out, &mut self.secret_guy.data_out);
}
}
As a parting note, you can make interfaces generic across types. Here, for example is the FIFO interface used in the High Level Synthesis library in RustHDL: ```rust
pub struct FIFOWriteController
pub struct FIFOWriteResponder
You can then use any synthesizable type for the data bus, and keep the control signals as single bits! Neat, eh? 🦑
Occasionally in RustHDL, you will need to wrap an external IP core or logic primitive supported
by your hardware, but that is not supported directly in RustHDL. There best method for wrapping
Verilog code is to use the Wrapper struct and provide your own implementation
of the hdl
method for your logic.
Here is a minimal example of a clock buffer primitive (that takes a differential clock input
and provides a single ended clock output). The Verilog module declaration for the clock buffer is
simply:
verilog
module IBUFDS(I, B, O);
input I;
input B;
output O;
endmodule
Since the implementation of this device is built into the FPGA (it is a hardware primitive), the module definition is enough for the toolchain to construct the device. Here is a complete example of a wrapped version of this for use in RustHDL.
```rust
pub struct ClockDriver {
pub clockp: Signal
impl Logic for ClockDriver {
// Our simulation simply forwards the positive clock to the system clock
fn update(&mut self) {
self.sysclock.next = self.clockp.val();
}
// RustHDL cannot determine what signals are driven based on the declaration
// alone. This method identifies sys_clock
as being driven by the internal
// logic of the device.
fn connect(&mut self) {
self.sysclock.connect();
}
// Normally the hdl
method is generated by the derive
macro. But in this
// case we implement it ourselves to wrap the Verilog code.
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 ibufdsinst(.I(clockp), .B(clockn), .O(sys_clock));
"#.into(), // Some synthesis tools (like [Yosys] need a blackbox declaration so they // can process the Verilog if they do not have primitives in their // libraries for the device. Other toolchains will strip these out. cores: r#" (* blackbox *) module IBUFDS(I, B, O); input I; input B; output O; endmodule"#.into(), }) } } ```
License: MIT