TxHDL today: a Rust HDL, its parts, and what runs on it

#TxHDL#Rust#RISC-V#FPGA#Formal verification#FireSim#Linux#auto

TxHDL is a hardware description language that is a Rust library, and a complete system ready to use. Its standard library holds the parts a system on chip needs: buses, a network on chip, memory, an MMU, Ethernet, video, debug and system control. On top of them run a RISC-V core that boots Linux, a GPU and a FireSim host link. Simulation, formal proof and the official RISC-V tests check all of it. This page describes TxHDL as it stands in November 2026. The earlier posts show it at the stages it passed through.

What TxHDL is

TxHDL describes digital hardware with ordinary Rust. A transaction is a struct. A hardware module, a unit, is a struct that implements the Unit trait. A pipeline is an async fn, and each .await in it is a clock edge. A configuration is a trait bound, and a clock is a type.

From one Rust source file, the library gives you three things:

  • a simulation, which is the Rust program itself, run by the library’s own executor;
  • Verilog and VHDL, written by the #[lower] attribute;
  • a check that the two netlists behave the same as the Rust run, cycle by cycle.

TxHDL is a project of Dragiša Janković and Filip Filmar. Its source is open, under the Apache 2.0 license, at HDL/txhdl, with a mirror on GitHub. We used Claude, a large language model, as an assistant throughout. Every commit says so and records its prompts.

Rust as a hardware description language

High-level synthesis (HLS) tools, such as Siemens Catapult for C++ and Google’s XLS for its Rust-like DSLX, read an algorithm and decide the hardware for you: how many cycles, which registers, how much sharing. TxHDL does not decide anything. You write each register and each cycle, as you would in Verilog. The Rust compiler checks the structure, and the library writes the netlist you described.

Here is a multiply-accumulate with a one-cycle multiplier, from the examples:

#[pipeline(mul = 1, add = 1)]
async fn mac(a: U<32>, b: U<32>, c: U<64>) -> U<64> {
    let p = mul(a, b).await;
    add(p, c).await
}

The attribute states how many cycles each operator takes. The two .awaits are the two register stages, and the lowered netlist has exactly those two.

.await works as a pipeline tick because of how Rust compiles an async fn. The compiler splits the function at each .await into a state machine, and keeps every value that lives across an .await in the machine’s state. In hardware, that state is the pipeline register, and moving from one state to the next is the clock edge. The simulation runs Rust’s own state machine, one step a cycle.

.await hides how long the wait is, so it abstracts the pipeline completely. A process can await the next edge of a clock, a value on a channel, or the response to an AXI burst it sent. The code reads the same in each case, and the lowering writes the handshake or the state machine that waits. TxHDL adds no cycles of its own: the netlist has a register where the code waits, and nowhere else. An HLS tool aims to translate an algorithm correctly, and often spends extra cycles to do so. In TxHDL you decide where every cycle goes, so you can chase the best performance the design allows.

The compiler also produces the lowering. #[lower] is a procedural macro: while rustc compiles a unit, the macro reads the unit’s source and writes the code that emits its Verilog and VHDL. A loop that waits for a clock edge becomes a clocked block, and a loop with several waits becomes a state machine with a state per wait. The macro refuses anything it cannot lower, at compile time, with a message that names it. Chisel works the other way: it finds the circuit by running the Scala program that builds it, so a mistake shows up when that program runs.

Rust’s type system does several checks that other HDLs leave to a lint tool or to the first board run:

  • A wire has one driver, because a wire’s end is a value that moves. A second driver is a compile error.
  • A signal crosses clock domains only through ChanCdc. A crossing elsewhere is a type mismatch, since each clock is its own type.
  • Combinational logic is a plain fn, and a plain fn cannot .await, so it cannot hold a register by mistake. A function’s type says whether it takes a cycle.

Interfaces from traits

Traits give each interface its natural shape. interface! declares a bus once, with its signals and the roles that use it:

interface! {
    Wishbone {
        adr: Signal<Data<U<32>>>,
        ack: Signal<Data<Bit>>,
        dat: Chan<Beat>,
    }
    role Initiator { out adr, in ack,  out dat }
    role Target    { in adr,  out ack, in dat }
    role Monitor   { in adr,  in ack,  in dat }
}

The macro generates a port type for each role. It refuses a role that leaves out a member, and two roles that both drive one. The AXI link works the same way. axi() returns a host end and a peripheral end, as chan() returns a sender and a receiver. The host calls a method to send a burst and awaits its response. It never handles the five channels, the beat counters or the transaction IDs itself. An address map is a trait constant, and a design’s configuration is a trait bound, so a unit states what it needs from its parameters in its type.

A compiler we did not have to write

A new language usually means a new parser, a type checker, error messages, an editor plugin and a package manager. TxHDL has none of these of its own. rustc is the parser and the checker, rust-analyzer is the editor support, and Cargo and Bazel fetch the dependencies. This let us skip language development and go straight to building hardware.

We built on the work of many engineers who came before us. Rust’s compiler and tools are the work of a large community. The ideas in TxHDL come from the hardware languages, simulators and synthesis tools of the last decades. Those engineers worked the concepts out and distilled them, and we fitted them together into TxHDL.

Rust also has language features for the work around the hardware, and TxHDL uses them instead of side tools:

  • Procedural macros give the syntax: #[lower] on 451 units, station!, regmap!, interface!, and derives such as Value, Trace and Ports.

  • Hygienic macros add what the language lacks. Rust has no named type parameters, so #[named_type_params] adds them: a bus type reads Bus<Addr<u32>, Width<u64>, Burst<u8>>, and two parameters given in the wrong order are a compile error where they are written. When stable Rust supports named type parameters itself, TxHDL will move to the language’s own form.

  • Attributes state what synthesis must know about a register or a memory. #[async_reg] marks a synchroniser’s stages, and the netlist marks them ASYNC_REG, so Vivado places them together. #[ram_style("block")] asks for block RAM.

  • Code generators are ordinary Rust programs that read the design. Every type is declared once, in Rust, and the rest is generated from it:

    • each peripheral’s register map, down to the bits of each field, from its regmap! declaration;
    • that map as a C header and as a Rust module, so drivers in either language use the same offsets and bit positions;
    • a device tree node for each peripheral, and the board’s whole device tree from the address decoding the design actually does;
    • each component’s datasheet, from its ports, state and parameters.

    Nobody types an offset twice, and the software cannot drift from the hardware. This is how Linux and Zephyr run on the board: their drivers include the generated headers, and their device trees come from the design. You can boot the same Linux on the board, or build a Zephyr image for it with west, the same way as for any other Zephyr board.

Synthesis and layout for a technology node need these declarations. In TxHDL they are part of the source, in the same language, and the compiler checks them.

AI assistance, and agent readiness

We built TxHDL with an AI coding assistant, and it multiplied our work because we gave it substance. We made the design decisions, wrote the specification and chose what to measure. The assistant wrote code and documents against them, and every claim had to pass a test, a proof or a board run before it stayed. Without that substance, an assistant produces volume. With it, the result has the tests, proofs and documents that used to take a much larger effort.

Rust’s strict rules helped the assistant too. An agent makes the same mistakes a person does, only faster. In TxHDL many of those mistakes stop the compile, before any simulation runs:

  • A second driver on a wire is a borrow-checker error.
  • A clock-domain crossing outside ChanCdc is a type error.
  • A bus role that drives a member it should only read is refused by interface!.

The agent reads the compiler’s message, fixes the code and tries again, in seconds. In Verilog the same mistakes compile, and show up later as a wrong waveform or a failed board run. The tests catch the rest: the co-simulation, the formal proofs and the conformance report.

Where TxHDL comes from

Embedding a hardware language in a general-purpose language is an old idea, and TxHDL builds on the projects that did it first.

  • RHDL embedded hardware description in Rust, with the clock domain as a type parameter. TxHDL took that idea directly, and RHDL is its closest ancestor.
  • Chisel and SpinalHDL embed hardware construction in Scala. A module is a class, and running the program elaborates the circuit. TxHDL’s structural half follows the same plan in Rust.
  • Python has MyHDL, Migen and its successor Amaranth, and LiteX, which builds whole systems on chip from Migen. cocotb writes testbenches in Python for any simulator.
  • Bluespec describes hardware as guarded atomic actions and lets its compiler schedule them.

Newer projects such as Spade and Veryl take a different route. They define a Rust-like language and build a compiler for it. A compiler project is worthwhile work, and these projects advance the field. The compiler becomes the main project, though: each needs its own parser, type checker, error messages and editor support before a production design can rely on it. We wanted to build hardware. TxHDL uses Rust itself, so Rust’s compiler and tools already do that work, and our time went into the hardware.

TxHDL reuses ideas from all of these projects. What it changes is when the work happens. Checks and transformations that other systems do while their generator program runs, or in a compiler of their own, happen in TxHDL while rustc compiles the design. That moves them earlier, into tools that millions of Rust programmers already use every day.

The runtime library

The library has two layers. The runtime is the language itself. It has types such as U<N> and Bit, units and wires, channels, the executor and the lowering. It also has check!, assume! and cover! for formal proof, regmap!, and foreign units, which run a Verilog or VHDL module beside the Rust ones.

The runtime is small and plain. Its core, without the macros, is about 12,000 lines of Rust. The executor has no event queue and no delta cycles. One poll of the top unit is one clock tick: the executor polls every process once, and applies every drive at the end of the step. A designer can read the whole time model in one page of its source.

Because the simulation is an ordinary Rust program, hardware and software run together in it with no glue. A test can call any Rust code: a reference model, a file parser, a network library. The Remote part takes this further. It sits on the bus as a peripheral and sends each transaction it receives to software. In simulation the software is a Rust function in the same program. On the board it is a program on another machine, and each transaction travels as an Ethernet frame. A new peripheral can be written as software first, against the real bus and the real system, and replaced by hardware once it works.

The parts are the standard library: reusable hardware, written in the language itself, and ready to drop into a new design. They are what hardware engineers have long built by hand in Verilog and VHDL: buses, arbiters, FIFOs, clock-domain crossings, caches and peripherals. We wrote them again in TxHDL, from the protocols and techniques that other engineers defined and refined. Each part has its own tests, and most have run on the board inside a working system. No part uses a compiler feature that a user cannot, so a user’s own parts work the same way.

Area Parts
Buses AXI4 link, router, round-robin arbiter, AXI-Lite and Wishbone bridges, protocol trackers, interrupt controller
Network on chip node, switch, AXI bridges, a mesh with request and response virtual channels
Memory buffers, FIFOs, a clock-crossing FIFO, memory engines, a set-associative table, flash read as memory
Processor support Sv32 translation with fetch and data TLBs, reservation stations of 2 to 10 inputs
I/O Ethernet MAC and its frame engines, HDMI output, I2C, SPI, SD, MDIO, GPIO, PWM
System system control, windowed watchdog, trace buffer, entropy source with health tests, the RISC-V debug transport
Redundancy a tee and a check, to run one design twice in lockstep
Remote a peripheral written as software, reached over Ethernet; FireSim’s host and stream links

The whole project is about 135,000 lines of Rust. About 25,000 are the runtime and its macros, and 33,000 are the parts. The Vreteno core and its board are 40,000, the 138 examples 22,000, and the GPU, the DDR3, Ethernet, HDMI and PCIe designs and the combined SoC 16,000.

What we built with it

Vreteno is an RV32IMAC core with machine, supervisor and user modes, Sv32 virtual memory with two 8-entry TLBs, a 16 KB instruction cache and a 4 KB data cache. It runs at 100 MHz on an Alinx AX7A200B, an Artix-7 board with 1 GiB of DDR3. The DDR3 controller is AMD’s, wired in as a foreign unit. It is the only part of the board design that we did not write in TxHDL.

Vreteno is dual-core on the board. Two harts share the DDR3 through the bus arbiter. They have the atomic instructions, both load-reserved and store-conditional and the nine atomic memory operations, and all their conformance tests pass. A hart’s data cache drops any line the other hart writes. Linux runs on both harts. Programs split work between them: in the 3D demos one hart builds the next frame’s display list while the other bins the current one into tiles.

Razboj is the GPU. It is a second bus master beside the core. Software writes a display list into DDR3 and rings a doorbell register, and Razboj draws into the frame buffer that the HDMI output shows. It rasterises one pixel a clock from three edge functions and writes each span in AXI bursts of up to 16 beats. In tiles held in block RAM it tests depth and stencil, blends, and textures through a 64-line cache, with OpenGL’s filters, mipmaps, two texture units and texture combining. Its output matches its software model byte for byte.

On the core, an OpenGL ES 1.1 Common-Lite library feeds Razboj. Common-Lite is the fixed-point profile, which suits a core without floating point. The library does the transforms, lighting and clipping, and offers a C API and EGL. Its conformance suite, which includes tests taken from piglit, passes with no known failures.

The network on chip joins hosts and peripherals on a 2D mesh. In the demonstration, Vreteno, Razboj, a shared memory and a serial port sit on a 2 by 2 mesh. The core writes a display list over the network and the GPU draws it.

The flagship is one bitstream with the core, DDR3, Ethernet, HDMI video and Razboj, routed for the board and run on it.

Built for production use

Every netlist is simulated twice

Each lowered unit is simulated as VHDL under nvc and as Verilog under Verilator. Both runs must match the trace the Rust run recorded. bazel test //... runs 921 targets, among them 312 co-simulated netlists. Vreteno runs in lockstep with a software model of the instruction set: after every instruction the program counter, all 31 registers and the privileged state must agree.

Formal verification

A unit states its invariants with check!, its assumptions about its inputs with assume!, and the states it must reach with cover!. formal_test writes them into the netlist and hands it to SymbiYosys. Property-directed reachability proves each invariant for every reachable state, not only the states a test visits. Each suite also injects a bug and expects a counterexample, so a proof that passes for the wrong reason fails. The ten suites finish in seconds.

RISC-V conformance, every day

RISC-V International’s architectural tests run on Vreteno’s Verilog netlist every day, with the Sail reference model as the judge. In the report of 5 November, all 156 tests that apply pass, across 28,308 checked results. The first run failed one test. It found that the core could not set mstatus.TVM, and that is fixed.

FireSim

FireSim runs a RISC-V system on an FPGA at speed, while a host steps and reads it. Its flow used to need sbt, conda and a Vivado project on the host. We built it with Bazel instead, from source, and published it as bazel_rules_firesim, a separate Golden Gate module, and 22 modules in our registry.

TxHDL wrote the host link. FireSim normally talks to its FPGA over PCIe. On the AX7A200B, TxHDL parts send FireSim’s register and stream traffic in Ethernet frames instead. With that link, FireSim’s Rocket design printed its greeting on the board, and Linux 6.18 booted on the simulated Rocket core.

Works with existing tools

TxHDL fits into the tools a hardware team already has. It uses Verilog and VHDL as an intermediate representation. Designers write Rust, with its editors, its package manager and its checks. The vendor and open tools receive the Verilog or VHDL they expect. Nobody has to write those languages by hand, or wait for their tools to modernise.

  • Its output is plain Verilog and VHDL, which Vivado, Yosys, OpenROAD, Verilator and nvc read as they read hand-written code.
  • A foreign unit brings an existing Verilog or VHDL module into a TxHDL design and simulates it beside the Rust units.
  • Vendor code generators keep working. The board’s DDR3 controller comes from AMD’s Memory Interface Generator (MIG), and its PCIe endpoint from AMD’s XDMA generator. Each enters the design as a foreign unit.
  • Encrypted IP keeps working too, because the design goes through Vivado as usual. The DDR3 controller’s physical layer uses AMD’s encrypted parts. Vivado synthesises them, and Vivado’s own simulator runs the tests that include them, as in any other design. The open simulators run the rest.
  • The buses are standard AXI4 and AXI-Lite, with a bridge to Wishbone for existing cores.
  • On the software side, the design produces C headers and device trees, so Linux, Zephyr and OpenSBI find the hardware the way they find it on any other board. OpenOCD, gdb and Android’s fastboot work with it as they are.
  • FireSim’s own drivers run against the TxHDL host link.

You give up none of your existing tools, IP or know-how by using TxHDL. You keep your toolbox, and add a modern tool to it, with batteries included.

Hermetic, ephemeral, reproducible builds with Bazel

Bazel builds everything, and it fetches every tool at a pinned version and checksum: Rust, the C and C++ compilers, nvc, Verilator, SymbiYosys and z3, Yosys and OpenROAD, OpenOCD and gdb, and TeX for the documents. Vivado comes from AMD’s installer, and only the targets that need it run it. Nothing is installed by hand. A build machine needs Bazelisk and Git, so it can be thrown away and replaced at any time. Our own build machines are short-lived cloud machines. A tutorial starts from an empty directory with five files and builds a blinky against the published library.

The project also insists that the same sources give the same bytes, whoever builds them and wherever. You do not need this to use TxHDL, but professional hardware work does: a team must be able to rebuild last year’s bitstream, and a cache must be able to trust a result built on another machine. The Linux and user-space builds fix their time stamps and time zone. A test fails if OpenSBI’s image or the FireSim payload names the directory it was built in. Bazel’s shared cache reuses a result only when every input matches, so one machine’s results serve the others and CI.

Debugging the board

Vreteno has the standard RISC-V debug transport, behind the FPGA’s JTAG. OpenOCD and gdb halt the core, read its registers and set breakpoints on the board.

Toward silicon

The same Vreteno netlist that Vivado reads also goes through Yosys and OpenROAD onto the open Nangate45 cell library. The result is a 293 µm square die with about 17,000 cells, routed at a 3 ns clock (333 MHz) with positive slack. The project’s tapeout document lists what a real tapeout would still need.

Workflows done end to end

Each of these has run from start to finish, and most of them run again in the test suite on every change:

  • From Rust to the board. A unit is written in Rust, simulated, lowered to Verilog, synthesised, placed and routed by Vivado, and programmed into the FPGA. On the board, Vreteno prints on its serial port what its simulation printed. The same design is written to the board’s flash and starts after a power cycle.
  • From Rust to a chip layout. The same Vreteno netlist goes through Yosys and OpenROAD to a routed layout.
  • Two simulators against one trace. Every lowered unit runs under nvc and Verilator, against the trace of its Rust run.
  • Proof. Properties written in the unit are proved by SymbiYosys, and an injected bug produces a counterexample.
  • Conformance. The official RISC-V tests run on the netlist every day, judged by Sail, and the report is published.
  • Debugging. OpenOCD and gdb halt the core on the board, read its registers and load programs over JTAG. Vivado’s logic analyser captures signals inside the FPGA.
  • Software bring-up. Rust and C programs, Zephyr and Linux are built with the pinned toolchains, loaded over the serial line or the network, and run on the board.
  • FireSim. A Chisel design goes through Golden Gate to a bitstream, and FireSim’s driver runs it over Ethernet, up to a Linux shell on the simulated Rocket core.

What runs on it

Each of these workloads runs on the board, and most of them run in the test suite too.

  • Linux. Mainline Linux 6.18 on OpenSBI, with both harts, virtual memory, a BusyBox shell, networking and a root file system on NFS.
  • Zephyr. Zephyr 4.1 with our Ethernet driver, an entropy driver for the network stack’s random numbers, and a fastboot server, so a stock fastboot loads programs over the network.
  • Doom. Freedoom draws into a DDR3 framebuffer and the board shows it on HDMI, at 6 to 7 frames a second in the first level. The core does all the work in software.
  • 3D graphics. Ten icosahedron demos start with flat triangles, and each of the next nine adds one OpenGL feature: depth, smooth shading, textures, mipmaps, fog, blending, mirroring, two texture units and texture combining. The Utah teapot draws at 3.3 frames a second on two harts.
  • Programs from the host. Bare-metal Rust and C programs, loaded over the serial line or the network.

What is not done

  • An instruction cache miss stalls the pipeline until the read returns.
  • The network on chip splits bursts into single beats and refuses wrapping bursts.
  • Razboj draws one pixel a clock, and does depth, blending and textures only in tiles.
  • The PCIe endpoint has run in simulation, not yet on the board.
  • One board, the AX7A200B, has run every design. FireSim’s flow supports one platform.
  • The ASIC layout is a demonstration and needs more work before a tapeout.

Each of these has an issue in the repository.

Where to read more