RustHDL

repository·main·Indexed 19 days ago

https://github.com/samitbasu/rust-hdl

A framework for writing FPGA firmware using a subset of the Rust programming language, which compiles into Verilog for synthesis. RustHDL provides a strongly typed interface to hardware design, utilizing attributes like #[derive(LogicBlock)] and #[hdl_gen] to define circuits and logic. It includes tools for simulation via the simple_sim! macro and utilities to visualize waveforms by converting VCD files to SVG.

Tokens
85.4K
Snippets
275
Records
324
Agent score
67%

What's inside rust-hdl

  1. What is RustHDL and why use an FPGA?

    main

    RustHDL is a tool designed to make FPGA programming less difficult by providing a more accessible interface to hardware design.

    Why use an FPGA instead of a CPU?

    While CPUs are faster in raw clock speed and better for flexible, sequential tasks (the "Kitchen Analogy"), FPGAs are massively parallel devices (the "Food Factory Analogy"). FPGAs excel in scenarios where a CPU struggles:

    • Deterministic Systems: FPGAs can execute multiple tasks simultaneously at precise intervals without the pre-emption or scheduling jitter found in standard Operating Systems or even RTOSes. This is critical for high-frequency sampling (e.g., measuring multiple sensors at different, non-integer rates).
    • High Speed I/O: FPGAs can handle high-speed, predictable data streams (like MSPS radio signals) by using dedicated high-speed I/O circuitry and converting narrow, fast data streams into wider, slower streams that are easier to process.
    • Low Power Designs: FPGAs can be optimized for specific hardware tasks to maintain efficiency.
    • Cybersecurity: Because FPGA designs are composed of smaller, simpler components in complex topologies rather than massive, complex software ecosystems, they can be harder to subvert or repurpose via software-based attacks.
  2. Overview of RustHDL

    main

    RustHDL is a crate designed for writing FPGA firmware using the Rust programming language. It works by compiling a subset of Rust into Verilog, enabling you to use standard FPGA synthesis tools with your Rust-based designs.

    Key capabilities include:

    • Synthesis: Compiles a subset of Rust to Verilog for FPGA synthesis.
    • Simulation & Verification: Provides tools for testing and verifying hardware logic.
    • Analysis: Includes tools for analyzing designs.
    • Strongly Typed Interfaces: Uses Rust's type system to ensure design correctness before hardware deployment.
  3. External tools and inspirations used by RustHDL

    main

    RustHDL integrates with or is inspired by several key hardware description and FPGA toolchains:

    • YoSys: Used by RustHDL for Verilog synthesis and static analysis to check designs for potential errors.
    • Icarus Verilog: A software-based, open-source Verilog simulator used for design verification.
    • Verilator: A professional-grade simulation tool. RustHDL aims to generate Verilog compatible with Verilator for high-performance simulation.
    • OpalKelly FrontPanel API: RustHDL provides bindings to the OpalKelly FrontPanel API, making it easy to interface with OpalKelly FPGA modules.
    • MyHDL: A Python-based HDL generation approach that influenced some of RustHDL's conceptual design.
    • LucidHDL & AlchitryLabs: Projects from Alchitry that served as early inspirations for the development of RustHDL.
  4. Explore FPGA programming models in RustHDL

    main

    RustHDL provides guidance and support for several different FPGA programming models. You can choose an approach based on your familiarity with hardware description languages or your preference for high-level programming languages:

    • Verilog: A traditional approach adapted from simulating circuits to describing them.
    • Lucid: An approach focused on improving ergonomics and Developer Experience (DX) compared to Verilog.
    • Python-based (MyHDL/MiGen/Python): Approaches that use Python to describe firmware and hardware logic.
  5. Key features of RustHDL

    main

    RustHDL is a hardware description language (HDL) implemented in Rust that provides several core benefits for firmware and hardware design:

    • Safety: Uses Rust's strongly typed interfaces to check validity at compile time, run time, synthesis, and on the device.
    • Performance: Allows running simulations of designs directly from Rust code with high performance.
    • Readability: Generates readable Verilog code for synthesis and implementation, facilitating timing and conflict resolution.
    • Reusability: Supports parametric firmware via templates and a composition model based on Rust structs.
    • Batteries Included: Provides a library of basic firmware widgets including FIFOs, RAMs, ROMs, Flip-flops, SPI components, and PWMs.
    • Open Source: All RustHDL code and firmware are open source and free to use.
  6. What is RustHDL and how does it work?

    main

    RustHDL is a crate that allows you to write FPGA firmware using a subset of Rust. It compiles your Rust code into Verilog, which can then be synthesized using standard FPGA tools.

    Core Workflow

    1. Model Circuits: Use Rust structs to represent circuits, composed of sub-circuits (widgets) and signal wires.
    2. Define Logic: Implement the Logic trait for your struct and provide an update(&mut self) method. This method acts as the HDL kernel.
    3. Annotate: Use #[derive(LogicBlock)] on your struct and #[hdl_gen] on your update method to enable simulation and Verilog generation.
    4. Simulate & Synthesize: Run simulations directly in Rust or use a Board Support Package (BSP) to generate bitstreams for specific hardware.
  7. Use High Level Synthesis (HLS) wrappers in RustHDL

    main

    RustHDL provides High Level Synthesis (HLS) wrappers to simplify the connection of complex core widgets. While core widgets like AsynchronousFIFO expose many individual signals (e.g., 14 separate signals for a FIFO), HLS wrappers group these into manageable bus-like structures.

    These HLS constructs are thin wrappers; they do not add overhead during synthesis or signal plotting, as they map directly to the underlying core signals.

    # use rust_hdl::prelude::*;
    // The HLS wrapper simplifies the 14 signals of AsynchronousFIFO into 4 signals
    pub struct AsyncFIFO<T: Synth, const N: usize, const NP1: usize, const BLOCK_SIZE: u32> {
        pub bus_write: FIFOWriteResponder<T>,
        pub write_clock: Signal<In, Clock>,
        pub bus_read: FIFOReadResponder<T>,
        pub read_clock: Signal<In, Clock>,
        fifo: AsynchronousFIFO<T, N, NP1, BLOCK_SIZE>,
    }
  8. Understand how loops work in RustHDL

    main

    Loops in RustHDL follow hardware design principles rather than software execution principles. A for loop does not represent a temporal loop in the hardware. Instead, it is used for unrolling: it is a way to repeat a block of code multiple times during the HDL kernel generation process with a varying parameter.

    When you use a for loop in an impl Logic block, the generator unrolls the loop to create the corresponding hardware connections or logic for each iteration.

    impl<const N: usize> Logic for PulserSet<N> {
        #[hdl_gen]
        fn update(&mut self) {
            // This loop is unrolled by the generator to connect N clocks
            for i in 0..N {
               self.pulsers[i].clock.next = self.clock.val();
               self.pulsers[i].enable.next = true.into();
            }
        }
    }
  9. Define a LogicBlock for simulation and synthesis

    main

    To create a hardware module in rust-hdl, define a struct and derive the LogicBlock macro. This macro enables the struct to be used for both simulation and Verilog synthesis.

    Inside the struct:

    • Use Signal<In, Clock> for clock inputs.
    • Use Signal<Out, Bit> for single-bit output signals.
    • Sub-circuits (widgets) can be included as fields.

    Implement the Logic trait for your struct and use the #[hdl_gen] attribute on the update function. This attribute transforms the update function into an HDL Kernel that can be converted into Verilog. Within update, use .next to write to a signal and .val() to read its current value.

    #[derive(LogicBlock)]
    struct Blinky {
        pub clock: Signal<In, Clock>,
        pulser: Pulser,
        pub led: Signal<Out, Bit>,
    }
    
    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();
        }
    }
  10. Use arrays to handle variable inputs or outputs

    main

    In RustHDL, you can handle a variable number of inputs, outputs, or subcircuits by using arrays. You can implement these using a statically sized array (via a const generic parameter) or a vec.

    As long as the members of the array implement the Block trait (meaning they are circuits), they will work correctly during both simulation and synthesis. This is useful for creating processing stages with variable passes, muxes with variable inputs, or banks of identical state machines.

    # use rust_hdl::prelude::*;
    
    struct PulserSet<const N: usize> {
        pub outs: Signal<Out, Bits<N>>,
        pub clock: Signal<In, Clock>,
        pulsers: [Pulser; N]
    }
  11. Understand the challenges of high-level FPGA generators

    main

    When using high-level languages (like Python) to generate hardware (Verilog), developers face two primary challenges that RustHDL seeks to mitigate:

    1. Lack of Compile-Time Correctness

    In dynamically typed languages like Python, syntactic errors or misspelled signal names may only be discovered during runtime/execution. In contrast, strongly typed languages like Rust provide contracts that allow developers to focus on logic and edge cases rather than basic syntax errors.

    2. Transparency of Generated Code

    High-level abstractions can hide hardware complexity. A few lines of high-level code might generate a circuit that is physically impossible to build or that violates timing constraints (e.g., requesting complex operations like division or dynamic indexing that the FPGA cannot perform efficiently). When the FPGA toolchain fails, the developer must often debug the generated Verilog. If the abstraction is too thick, the resulting Verilog may contain 'funkafied' identifiers or monolithic functions that are difficult to map back to the original source.

  12. How RustHDL simulation ownership works

    main

    RustHDL's simulation model is designed to allow concurrent testbenches without shared mutable state by leveraging Rust's ownership system:

    1. Threaded Execution: Each testbench runs in its own thread.
    2. Circuit Ownership: The simulation engine holds the circuit in a Box.
    3. The Endpoint (ep): The circuit is moved to the testbench via an endpoint. The testbench becomes the sole owner of the circuit during its execution phase.
    4. Handover: The testbench updates inputs and checks outputs, then moves the circuit back to the simulation engine using ep.done(x).
    5. Termination: The simulation halts when all testbenches are complete or any testbench reports an error.