Design hardware using Ruby's expressive syntax and metaprogramming. Familiar language, powerful abstractions for digital circuit design.
Traditional HDLs like Verilog and VHDL were designed decades ago with verbose, rigid syntax. Ruby brings a modern, expressive language to hardware design — one that engineers already know and love. RHDL leverages Ruby's metaprogramming to create a DSL that reads like a hardware specification but executes as real Ruby code.
| Aspect | Verilog / VHDL | RHDL (Ruby) |
|---|---|---|
| Syntax verbosity | High — explicit begin/end, wire declarations | Minimal — Ruby blocks and conventions |
| Parameterization | Limited generate/generic support | Full Ruby metaprogramming, loops, conditionals |
| Type system | Rigid, error-prone width mismatches | Dynamic with automatic width inference |
| Testbench | Separate testbench files, $display | RSpec integration, Ruby assertions |
| Code reuse | Copy-paste or limited includes | Modules, mixins, inheritance, gems |
| Tooling | Proprietary vendor tools | Open source, standard Ruby ecosystem |
A simple counter in Verilog requires explicit boilerplate for ports, wire types, and sensitivity lists.
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or
posedge rst) begin
if (rst)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
The same counter in RHDL is concise, readable, and parameterized naturally with Ruby.
class Counter < RHDL::Sim::SequentialComponent
input :clk, :rst, :en
output :count, width: 8
behavior clock: :clk,
reset: :rst do
if en
count <= count + 1
end
end
end
RHDL components are defined as Ruby classes that inherit from base component types. Inputs, outputs, and behavior blocks map directly to hardware concepts while remaining idiomatic Ruby.
Port declarations use Ruby symbols and keyword arguments. Width defaults to 1 (single bit) when omitted. Multiple ports can share a single declaration line.
class ALU < RHDL::Sim::Component
# Single-bit inputs (width: 1 is default)
input :carry_in
# Multi-bit inputs with explicit width
input :a, width: 8
input :b, width: 8
input :op, width: 4
# Multiple outputs on one line
output :result, width: 8
output :carry, :zero, :overflow
# Combinational behavior block
behavior do
result <= case_select(op, {
0 => a + b + carry_in,
1 => a - b,
2 => a & b,
3 => a | b,
4 => a ^ b,
})
zero <= result.eq?(0)
carry <= result[8]
end
end
Sequential components use clock and reset signals. State machines are first-class citizens with declarative transition rules.
class TrafficLight < RHDL::Sim::SequentialComponent
input :clk, :rst
input :sensor
output :red, :yellow, :green
state_machine :state,
clock: :clk, reset: :rst,
states: [:idle, :go,
:slow, :stop] do
transition :idle => :go,
when: sensor
transition :go => :slow,
after: 30
transition :slow => :stop,
after: 5
transition :stop => :go,
after: 20
end
behavior do
red <= state.eq?(:stop)
yellow <= state.eq?(:slow)
green <= state.eq?(:go)
end
end
Components are composed by instantiation. Wiring is explicit and type-checked, catching width mismatches at elaboration time.
class CPU < RHDL::Sim::Component
input :clk, :rst
input :data_in, width: 8
output :data_out, width: 8
output :addr, width: 16
# Instantiate sub-components
component :alu,
ALU.new
component :decoder,
Decoder.new
component :regfile,
RegisterFile.new(
num_regs: 8
)
# Wire components together
behavior do
decoder.opcode <= data_in
alu.a <= regfile.rd1
alu.b <= regfile.rd2
alu.op <= decoder.alu_op
regfile.wd <= alu.result
end
end
Ruby's metaprogramming capabilities enable hardware patterns that are impossible or extremely verbose in traditional HDLs. Generate parameterized components, create pipeline stages dynamically, and build entire architectures from configuration hashes.
Parameterized components use standard Ruby to generate hardware. No special generate syntax needed — loops, conditionals, and methods just work.
class FIFO < RHDL::Sim::SequentialComponent
def initialize(depth: 16, width: 8)
input :clk, :rst
input :wr_en, :rd_en
input :din, width: width
output :dout, width: width
output :full, :empty
# Generate storage registers
addr_bits = Math.log2(depth).ceil
register :wr_ptr, width: addr_bits
register :rd_ptr, width: addr_bits
memory :mem,
depth: depth, width: width
end
behavior clock: :clk,
reset: :rst do
if wr_en && !full
mem[wr_ptr] <= din
wr_ptr <= wr_ptr + 1
end
if rd_en && !empty
dout <= mem[rd_ptr]
rd_ptr <= rd_ptr + 1
end
end
end
# Instantiate with different configs
small = FIFO.new(depth: 4, width: 8)
large = FIFO.new(depth: 64, width: 32)
Dynamic generation lets you build entire bus architectures, pipeline stages, and interconnects from configuration data.
# Generate an N-stage pipeline dynamically
def build_pipeline(stages:, width:)
Class.new(RHDL::Sim::SequentialComponent) do
input :clk, :rst
input :din, width: width
output :dout, width: width
output :valid
# Create pipeline registers
stages.times do |i|
register :"stage_#{i}",
width: width
register :"valid_#{i}"
end
behavior clock: :clk,
reset: :rst do
# Chain stages together
send(:stage_0) <= din
(1...stages).each do |i|
send(:"stage_#{i}") <=
send(:"stage_#{i-1}")
end
dout <= send(:"stage_#{stages-1}")
end
end
end
# Create pipelines at elaboration time
Pipe3 = build_pipeline(stages: 3, width: 16)
Pipe7 = build_pipeline(stages: 7, width: 32)
Mixins and modules let you share behavior across components — a common pattern for adding debug interfaces, protocol support, or standard bus connectivity to any design.
module Debuggable
def self.included(base)
base.input :debug_en
base.output :debug_data, width: 32
base.output :debug_valid
end
def emit_debug(data)
if debug_en
debug_data <= data
debug_valid <= 1
end
end
end
class MyProcessor < RHDL::Sim::SequentialComponent
include Debuggable
# debug_en, debug_data, debug_valid ports are
# automatically added by the mixin
end