Register-Transfer Level

Register-transfer level (RTL) is the abstraction at which digital hardware is described in terms of registers (clock-synchronous state storage, almost always D flip-flops) and the combinational logic that computes the next value of each register from the current values of the others (Wikipedia: Register-transfer level). RTL sits above gate-level netlists (which name every AND, OR, and flip-flop in the implementation) and below algorithmic or behavioural descriptions (which describe what a block should compute without committing to clock-cycle structure). It is the input level at which modern hardware engineers actually write designs, and the output level that logic synthesis tools consume. Verilog, SystemVerilog, VHDL, Chisel, and SpinalHDL are all RTL hardware description languages (HDLs); they differ in syntax and ergonomics but target the same abstraction. For the definitely-not-esp32 project, every line of CPU and SoC source is RTL, written to be both simulatable in Verilator and synthesizable to the Tang Nano 20K’s FPGA fabric.

Mental Model

A digital circuit at RTL is a stack of layers separated by clock edges. Between any two clock edges, a cloud of pure combinational logic computes outputs from current register values and external inputs. On the next clock edge, the registers atomically capture whichever input was at their D pin. The next half-cycle’s combinational cloud sees the new register values and recomputes. The whole circuit is a finite-state machine whose state is “every flip-flop on the die” and whose transition function is “the combinational logic between flip-flops.”

RTL forces the designer to think in those terms. Every storage location is an explicit register declaration. Every computation lives in a wire (Verilog) or signal (VHDL) that resolves combinationally. Every cycle-by-cycle behaviour is encoded as “the next value of register X is some function of register Y and register Z.” The result is a description that maps unambiguously to a netlist of flip-flops and gates, which maps unambiguously to LUTs and BRAMs on an FPGA, which maps unambiguously to a configured silicon circuit. Each level of the stack is mechanically derivable from the level above.

flowchart TB
  ALGO["Algorithm / behavioural<br/>(C, SystemC, Bluespec)"] -->|"high-level synthesis<br/>(HLS), or manual rewrite"| RTL
  RTL["RTL<br/>(Verilog / SystemVerilog /<br/>VHDL / Chisel / SpinalHDL)"] -->|"logic synthesis<br/>(Yosys / Vivado / Quartus)"| GATE
  GATE["Gate-level netlist<br/>(AND, OR, NOT, DFF cells)"] -->|"technology mapping"| TECH
  TECH["Mapped netlist<br/>(LUTs + FFs + BRAMs +<br/>DSP slices for FPGA;<br/>standard cells for ASIC)"] -->|"place and route"| PHYS
  PHYS["Physical layout<br/>(switch settings on FPGA;<br/>polygons on ASIC mask)"] --> SILICON["Running silicon"]

  style RTL fill:#fff4c2,stroke:#333,stroke-width:2px

The digital design abstraction stack. What it shows: RTL is the middle of a layered pipeline; tools translate downward (RTL → gates → mapped → physical → silicon) automatically, while moves upward require either high-level synthesis (still immature in 2026) or manual abstraction. The insight to take: the line between “things humans write” and “things tools generate” sits at RTL; everything above is research or specification, everything below is automated.

What Counts as RTL

A description is RTL if and only if it satisfies two properties:

  1. State is explicit and clocked. Every bit of memory in the design corresponds to a named register that captures its input on a specified clock edge. There is no implicit memory, no Mealy-style stateful procedural code that hides flip-flops, no “compiler-inferred” latches without the designer noticing.
  2. Combinational logic is acyclic and bounded. The next-state function and the output function are pure Boolean functions of current state and inputs. They contain no loops (other than feedback through registers across cycles), they terminate in bounded time, and they map to a fixed circuit of gates.

In Verilog, this maps to a small set of disciplined patterns. always_ff @(posedge clk) blocks describe registers; their non-blocking assignments (<=) capture inputs at the clock edge. always_comb blocks describe combinational logic; their blocking assignments (=) update wires immediately. Continuous assign statements are combinational wires. Module instantiation composes hierarchies of these.

// A two-state FSM at RTL: a register, a combinational next-state function,
// and a combinational output function.
module loop_detector (
    input  logic clk,
    input  logic rst_n,
    input  logic in_bit,
    output logic detect       // pulses high when we see "10" two cycles in a row
);
    // (1) Explicit register: holds the previous input bit, captured on clk edge.
    logic prev_in_q;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) prev_in_q <= 1'b0;
        else        prev_in_q <= in_bit;     // next-state = current input
    end
 
    // (2) Combinational output: pure function of register state + input.
    assign detect = (prev_in_q == 1'b1) && (in_bit == 1'b0);
endmodule

The pattern is canonical: register declarations, an always_ff block describing the next-state transition, and assign (or always_comb) describing combinational outputs. Every Verilog RTL design, no matter how complex, decomposes into compositions of this pattern.

By contrast, the following is not RTL even though it is legal Verilog:

// Non-synthesizable behavioural code; uses time delays and procedural blocks
// that have no hardware analogue.
initial begin
    $display("starting");
    a = 1;
    #10 a = 0;        // delay 10 time units, then set a
    wait (b == 1);    // wait until b becomes 1
    $finish;
end

initial blocks, hash-delays (#10), wait statements, fork/join, and $display/$finish are simulation-only constructs. They are useful in testbenches but cannot be synthesized to gates; there is no FPGA primitive that represents “wait 10 nanoseconds.” Mixing them into the design (rather than the testbench) is the first thing the synthesizer will reject, with confusing errors.

The discipline of writing only synthesizable RTL, even when the immediate target is Verilator simulation, is what makes a definitely-not-esp32-style project actually port to the Tang Nano 20K at the end. Letting the RTL accumulate non-synthesizable constructs (“just for debugging”) leads to a late-stage rewrite when the FPGA build fails.

Why the Term

The “register-transfer” name comes from C. Gordon Bell and Allen Newell’s 1971 textbook Computer Structures: Readings and Examples, which introduced the Instruction Set Processor (ISP) notation for describing computers as “transfers of values between registers, with combinational logic on the wires” (Wikipedia: Hardware description language cites “C. Gordon Bell and Allen Newell’s 1971 work describing ‘register transfer level, first used in the ISP language to describe the behavior of the Digital Equipment Corporation (DEC) PDP-8’”). The name persists because it accurately describes the operational semantics: each clock cycle is a parallel set of “transfer the function of these registers into that register” assignments. Modern HDLs hide the literal “transfer” notation behind procedural blocks, but the underlying model has not changed in 55 years.

It is worth noting that RTL in software compiler contexts (GCC’s RTL intermediate representation) is unrelated; the name collision is unfortunate. The hardware RTL is the input level at which designers work, not an internal compiler representation. Wikipedia: RTL notes this explicitly: “in hardware design the RTL level is the usual input that circuit designers operate on” (whereas in software, RTL is below the user-visible level).

The HDLs That Target RTL

Multiple HDLs all express the same abstraction. The choice between them is one of ergonomics, ecosystem, and personal preference, not capability.

Verilog (IEEE 1364, originally 1985, standardized 1995, last update 2005). Syntactically similar to C, with module, wire, reg, and procedural blocks. The original dominant industry standard. Compact for small designs; verbose for large ones because it lacks parameterized types, packages, or structs in the original 1364 form.

SystemVerilog (IEEE 1800, since 2005, last update 2023). A strict superset of Verilog that adds packed structs, enums, logic (replacing the messy wire/reg distinction), always_comb / always_ff / always_latch (whose names communicate synthesis intent to the tool), interfaces, packages, generate blocks, and a large verification subset (classes, randomization, assertions, coverage) that is not synthesizable but is enormously valuable for testbenches (Wikipedia: SystemVerilog). The 2009 merge made Verilog formally a subset of SystemVerilog. For a 2026 project, writing the design in the SystemVerilog synthesizable subset is the right default; Verilog-95 is essentially legacy.

VHDL (IEEE 1076, originally 1987, last update 2019). Pascal/Ada-influenced syntax, strongly typed, verbose. Dominant in European industry, defence, aerospace, and academic teaching. Functionally equivalent to (System)Verilog at the RTL level; the trade-off is more characters per design but stronger type checking. For an open-source RISC-V project the Verilog/SystemVerilog ecosystem is much larger; VHDL would mean cutting the project off from most existing IP.

Chisel (Constructing Hardware in a Scala Embedded Language; UC Berkeley, since 2012). A domain-specific language embedded in Scala. The user writes Scala that, when run, generates RTL (specifically FIRRTL, which a separate compiler lowers to Verilog) (Wikipedia: Chisel). The win is Scala’s type system and functional programming for parameterized hardware generation: a 32-bit ALU and a 64-bit ALU can share one definition with a width parameter; complex bus crossbars can be generated programmatically. Used by the RISC-V Rocket chip, BOOM out-of-order core, and Google’s Edge TPU. Notable productivity data from the Chisel Wikipedia article: “some developers prefer Chisel because it requires one-fifth as much code and is much faster to develop than Verilog.”

SpinalHDL (since 2015). Also Scala-embedded, also generating Verilog/VHDL. Designed as a more pragmatic successor to Chisel: shorter syntax, faster compile times, better generated-Verilog readability. The widely-used VexRiscv RV32 core (which runs on the Tang Nano 20K) is written in SpinalHDL.

MyHDL (Python-embedded), PyMTL (Python), Amaranth (formerly nMigen, Python), Bluespec SystemVerilog (Haskell-influenced higher-level HDL) are smaller-community options. All target the same RTL level eventually.

The defining property of all of these: the user writes at RTL, the synthesizer reads RTL, the gate-level result is implementation-level. The choice of HDL changes the writing experience but not the abstraction.

Synthesizable RTL versus Behavioural Simulation Constructs

The hardware-description languages have a split personality. They include synthesizable constructs (what becomes hardware) and non-synthesizable simulation-only constructs (what runs only in a simulator). The split is empirical and pragmatic, not formal: it is whatever the synthesis tools have agreed to accept over decades of vendor competition.

Synthesizable (will become gates):

  • Register declarations: reg, logic (the SystemVerilog four-state type).
  • Combinational wires: wire, continuous assign.
  • always_ff @(posedge clk) and always @(posedge clk) blocks with non-blocking assignments (<=).
  • always_comb and always @(*) blocks with blocking assignments (=).
  • case, casez, if/else inside always blocks.
  • Bounded for loops with constant bounds (unrolled at synthesis).
  • Module instantiation and generate blocks.
  • Memory arrays (inferred as BRAM or distributed RAM).
  • Parameter and localparam declarations.

Not synthesizable (simulation-only):

  • initial blocks. These run once at simulation start; hardware does not have a “simulation start.”
  • #delay (hash-delay) statements. There is no FPGA primitive that delays a signal by 10 ns.
  • wait statements. No analogue.
  • fork/join parallel blocks (in the verification subset).
  • force, release on signals.
  • $display, $write, $fopen, $readmemh (in non-initialization contexts), $finish.
  • SystemVerilog classes, rand/randc, mailboxes, semaphores, dynamic arrays.
  • Concurrent assertion property language (most of it).

The discipline that pays off: keep design files (*.sv for the CPU, the bus, the peripherals) strictly synthesizable; keep testbench files (tb_*.cpp for Verilator, optionally tb_*.sv for events that need stronger SystemVerilog support) free to use the entire language. The synthesis tool then validates this separation by rejecting any non-synthesizable construct it finds in the design files.

Verilator helps enforce the discipline: it parses the synthesizable subset completely but rejects or warns on non-synthesizable constructs that have crept into the design (Verilator languages docs). A design that lints clean under verilator --lint-only -Wall is well on its way to also synthesizing cleanly.

Why Staying Synthesizable from Day One Matters

A common failure mode in hobbyist hardware projects: write the design with lots of behavioural constructs for “quick simulation,” plan to “clean it up for synthesis later,” and discover at FPGA-port time that “later” means a substantial rewrite of every stateful block. The cost of staying synthesizable from the first line is essentially zero; the cost of porting non-synthesizable RTL is days to weeks.

The specific failure patterns:

  • Inferred latches. An always_comb block that does not assign all outputs in every path infers a latch (a level-sensitive storage element). Synthesis tools usually warn; some FPGAs do not even have latch primitives, so the warning escalates to an error. Lint tools (Verilator, Synopsys SpyGlass, Cadence JasperGold) catch this early; doing so requires the design to actually be RTL.
  • Race conditions between blocking and non-blocking assignment. Verilog allows mixing = and <= in always blocks, but only <= is correct in a sequential block. Mistakes produce simulators that work and silicon that does not. The always_ff/always_comb SystemVerilog keywords let the tool catch the mistake; using plain always @(...) defers the check.
  • Unsynthesizable test hooks. Sprinkling $display inside the CPU’s main pipeline to debug a corner case “just for now” creates a design that simulates correctly and synthesizes wrongly (the $display is dropped, sometimes silently). The discipline is: never write a $display outside a testbench file.
  • Implicit clocks and multiple clocks crossing without synchronizers. Combinational paths that cross between two clock domains will produce metastability in silicon that does not appear in simulation. Synthesizable RTL discipline includes explicit clock-domain crossing synchronizers (two-flip-flop chains, async FIFOs) even when the simulator does not require them.

The definitely-not-esp32 project’s stated approach (Verilator first, FPGA after) only works if the RTL is synthesizable from day one. The investment is a discipline of using always_ff/always_comb, never using non-synthesizable constructs in design files, and running verilator --lint-only -Wall as part of the build. Everything compiles to gates if it was written to compile to gates.

Below RTL: Gate-Level and Beyond

Once RTL passes synthesis, the lower levels of the stack take over:

  • Gate-level netlist. A list of primitive gates (AND, OR, NOT, XOR, MUX, plus D flip-flops and latches) and their interconnections. The output of generic logic synthesis. Still vendor-independent.
  • Technology-mapped netlist. The gates are replaced by the specific FPGA’s primitives: 4-LUTs or 6-LUTs for combinational logic, the device’s flip-flop primitive (LUT4, LUT6_2, FDRE on Xilinx; ALU4, DFF on Gowin), BRAM macros for inferred memories, DSP slices for inferred multipliers. The same RTL targeting Xilinx vs Gowin vs Lattice produces different mapped netlists.
  • Placed and routed layout. Each primitive is assigned a specific physical CLB on the die; each signal is assigned to specific routing wires. The output is a database (a .dcp for Vivado, internal formats for others) that fully specifies the silicon-level configuration.
  • Bitstream. The placed-and-routed layout serialized into the device-specific binary format that the FPGA reads on configuration.

For an ASIC the path diverges at “mapped netlist,” substituting standard cells (a library of pre-characterized layouts for each gate type at a given silicon process) and producing a GDSII layout for mask manufacturing. The RTL is identical; only the tools and target library change. This is the second great virtue of RTL: a CPU written for FPGA prototyping at RTL can, in principle, be re-targeted to an ASIC fabrication run without rewriting a line.

Above RTL: High-Level Synthesis and Behavioural

There is an active research area in moving the design entry above RTL. High-level synthesis (HLS) tools (Xilinx Vivado HLS / Vitis HLS, Intel HLS, Mentor Catapult) accept algorithmic C or C++ and generate RTL automatically, scheduling operations into clock cycles and inferring registers. As of 2026 HLS is widely used in DSP and AI accelerator design where the algorithm has natural parallelism but is awkward to write directly at RTL; for control-heavy designs like CPU pipelines, HLS-generated RTL is still significantly slower and larger than hand-written RTL, so general-purpose CPUs remain RTL-authored.

Behavioural Verilog and SystemC sit between algorithm and RTL: cycle-accurate but with looser style requirements. Used in early-architecture exploration where the designer wants to see “what the algorithm looks like as hardware” before committing to a specific pipeline structure.

For the project at hand, the right altitude is RTL: low enough to control every register and to know the silicon you are building, high enough to express a complete CPU in a manageable number of lines.

See Also