rust-hdl

* 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.

Links

You may want:

Features

Quickstart

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;

[derive(LogicBlock)]

struct Blinky { pub clock: Signal, pulser: Pulser, pub led: 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: blinky<em>all Here is a zoom in showing the pulse to the LED blinky</em>pulse

The flow behind RustHDL is the following:

The rest is detail. Some final things to keep in mind.

Types

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.

Representing bits

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();

Operations

The Bits type supports a subset of operations that can be synthesized in hardware. You can perform

These 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.

Signal Type

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>, pub input2: Signal>, pub output: Signal>, } `` In this case, the fields of the adder circuit are marked aspubso 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,input1andinput2 should be considered inputs, andoutputis, obviously, an output. As such,My8BitAdderis promising you that it will drive theoutput` 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

[derive(Clone, Debug)]

pub struct Signal { pub next: T, pub changed: bool, // Internal details omitted } ```

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>, pub input2: Signal>, pub output: 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, pub val2: Signal>, pub val3: Signal>, pub val4: Signal, pub combined: Signal>, pad: 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.

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

use rust_hdl::core::prelude::*;

pub struct OrStuff {

pub val1: Signal,

pub val2: Signal>,

pub val3: Signal>,

pub val4: Signal,

pub combined: Signal>,

pad: Signal>,

}

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

[derive(LogicBlock, Default)]

struct BadActor { pub in1: Signal, pub in2: Signal, pub out1: 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() ```

Traits

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 { vec![] } } ```

The methods are quite simple:

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

[derive(LogicBlock)]

struct Blinky { pub clock: Signal, pulser: Pulser, pub led: 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.

The Synthesizable Subset of Rust and the HDL Kernel

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.

So this will clearly fail to compile.

```compile_fail

use rust_hdl::core::prelude::*;

struct Foo { bar: Signal> }

impl Logic for Foo { #[hdl_gen] fn update(&mut self) { self.bar.next = "Oy!"; // Type issue here... } } ```

```rust

struct Foo {}

impl Logic for Foo { #[hdl_gen] fn update(&mut self) { // Put your synthesizable subset of Rust here... } } ```

```compile_fail

use rust_hdl::core::prelude::*;

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; } } ```

So valid assignments will be of the form self.<signal>.next = <expr>, or for structure-valued signals.

struct Foo { pub sig1: Signal>, pub sig2: Signal>, pub sig3: 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 } } ```

struct Foo { pub sig1: Signal, pub sig2: Signal>, pub sig3: Signal>, pub sig4: Signal>, }

impl Logic for Foo { #[hdl_gen] fn update(&mut self) { self.sig2.next = 0.into(); // Latch prevention! // Straight ifs 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>, pub sig_index: Signal>, pub sig2: Signal, pub sig3: Signal>, pub sig4: 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

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

enum State { Idle, Running, Paused, }

struct Foo { pub start: Signal, pub pause: Signal, pub stop: Signal, pub clock: Signal, state: DFF, }

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, allforloops are unrolled at compile time, so they must be of the formfor in ..`. A simple example to consider is a parameterizable mux.

```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 { pub input_lines: [Signal; N], pub select: Signal>, pub outsig: Signal, fallback: Constant, }

// The impl for this requires a for loop impl Logic for Mux { #[hdlgen] fn update(&mut self) { self.outsig.next = self.fallback.val(); for i in 0..N { if self.select.val().index() == i { self.outsig.next = self.inputlines[i].val(); } } } } ``` RustHDL is still pretty restrictive about arrays and loops. You can still do great stuff though.

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!

[derive(LogicBlock)]

struct SPIMaster { // The pub members are the ones you can access from other circuits. // These form the official interface of the circuit pub clock: Signal, pub bitsoutbound: Signal>, pub dataoutbound: Signal>, // snip... // These are private, so they can only be accessed by internal code registerout: DFF>, registerin: DFF>, state: DFF, strobe: Strobe<32>, pointer: DFF>, // snip... // Computed constants need to be stored in a special Constant field member csoff: Constant, mosioff: Constant, }

impl Logic for SPIMaster { #[hdlgen] fn update(&mut self) { // Setup the internals - for Latch avoidance, each digital flip flop // requires setup - it needs to be clocked, and it needs to connect // the output and input together, so that the input is driven. // This macro simply declutters the code a bit and makes it easier to read. dffsetup!( self, clock, // | equivalent to self.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; } } } } } ```

Enums

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

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

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

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

enum State { Idle, Sending, Receiving, Done, }

struct Foo { dff: DFF, // <-- This is a 2 bit DFF } ```

Now imagine we add another state in the future to our state machine - say Pending:

```rust

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

enum State { Idle, Sending, Receiving, Pending, Done, }

struct Foo { dff: DFF, // <-- This is now a 3 bit DFF! } ``` RustHDL will automatically choose a 3-bit representation.

The 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

use rust_hdl::core::prelude::*;

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

enum State1 { On, Off, }

[derive(Copy, Clone, PartialEq, Debug, LogicState)]

enum State2 { Off, On, }

struct Foo { pub sigin: Signal, pub sigout: 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 structs for the write interface of the FIFO

```rust

[derive(LogicInterface)] // <- Note the LogicInterface, not LogicBlock

[join = "MyFIFOWriteSender"] // <- Name of the "mating" interface

struct MyFIFOWriteReceiver { pub datatofifo: Signal>, pub write: Signal, pub full: Signal, pub overflow: Signal, }

[derive(LogicInterface)] // <- Also here

[join = "MyFIFOWriteReceiver"] // <- Name of the "mating" interface

struct MyFIFOWriteSender { pub datatofifo: Signal>, pub write: Signal, pub full: Signal, pub overflow: Signal } ``` The names of the fields must match, the types of the fields must also match, and the directions of the signals must be complementary. So in general:

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>, pub read: Signal, pub empty: Signal, pub underflow: 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

[derive(Clone, Debug, Default, LogicInterface)]

[join = "FIFOWriteResponder"]

pub struct FIFOWriteController { pub data: Signal, pub write: Signal, pub full: Signal, pub almost_full: Signal, }

[derive(Clone, Debug, Default, LogicInterface)]

[join = "FIFOWriteController"]

pub struct FIFOWriteResponder { pub data: Signal, pub write: Signal, pub full: Signal, pub almost_full: Signal, } ```

You can then use any synthesizable type for the data bus, and keep the control signals as single bits! Neat, eh? 🦑

Simulation

Generating Verilog

Struct valued signals

Loops and Arrays

Wrapping IP Cores

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

[derive(LogicBlock, Default)]

pub struct ClockDriver { pub clockp: Signal, pub clockn: Signal, pub sys_clock: 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