A complete walkthrough of building an Arithmetic Logic Unit in RHDL, from Ruby source to synthesizable Verilog and gate-level netlist.
An ALU is the computational core of any processor. It takes two operands and an operation selector, then produces a result along with status flags. In RHDL, we model this as a purely combinational component -- no clock needed, just inputs flowing through logic to outputs.
class ALU < RHDL::Sim::Component
# Operand inputs: two 8-bit values
input :a, width: 8
input :b, width: 8
# Operation selector: 3 bits = 8 possible operations
input :op, width: 3
# Carry input for chained arithmetic
input :carry_in
# Result output: 8 bits wide
output :result, width: 8
# Status flags
output :carry_out # Carry/borrow from MSB
output :zero # Result is zero
output :negative # Result MSB is set
output :overflow # Signed overflow detected
# Operation constants for readability
OP_ADD = 0 # Addition with carry
OP_SUB = 1 # Subtraction with borrow
OP_AND = 2 # Bitwise AND
OP_OR = 3 # Bitwise OR
OP_XOR = 4 # Bitwise XOR
OP_SHL = 5 # Shift left
OP_SHR = 6 # Shift right (logical)
OP_NOT = 7 # Bitwise NOT (unary)
behavior do
# Extended result for carry detection (9 bits)
extended = case_select(op, {
OP_ADD => a.zero_extend(9) + b.zero_extend(9) + carry_in,
OP_SUB => a.zero_extend(9) - b.zero_extend(9) - ~carry_in,
OP_AND => (a & b).zero_extend(9),
OP_OR => (a | b).zero_extend(9),
OP_XOR => (a ^ b).zero_extend(9),
OP_SHL => concat(a, carry_in),
OP_SHR => concat(a[0], carry_in, a[7..1]),
OP_NOT => (~a).zero_extend(9),
})
# Drive outputs from the extended result
result <= extended[7..0]
carry_out <= extended[8]
# Flag computation
zero <= result.eq?(0)
negative <= result[7]
overflow <= (a[7] ^ result[7]) & ~(a[7] ^ b[7])
end
end
Each operation is selected by the 3-bit op input. The ALU computes all results in parallel through a multiplexer tree -- hardware does not "branch" like software. The case_select construct maps directly to a MUX in the final circuit. The 9-bit extended result lets us capture the carry output naturally without extra logic. Status flags (zero, negative, overflow) are derived combinationally from the result bits.
CIRCT compiles the Ruby ALU description into clean, synthesizable Verilog. The module interface maps directly from the RHDL inputs and outputs, preserving the same port names and widths.
module ALU (
input wire [7:0] a,
input wire [7:0] b,
input wire [2:0] op,
input wire carry_in,
output wire [7:0] result,
output wire carry_out,
output wire zero,
output wire negative,
output wire overflow
);
wire [8:0] extended;
assign extended =
(op == 3'd0) ? ({1'b0, a} + {1'b0, b} + carry_in) :
(op == 3'd1) ? ({1'b0, a} - {1'b0, b} - ~carry_in) :
(op == 3'd2) ? {1'b0, a & b} :
(op == 3'd3) ? {1'b0, a | b} :
(op == 3'd4) ? {1'b0, a ^ b} :
(op == 3'd5) ? {a, carry_in} :
(op == 3'd6) ? {a[0], carry_in, a[7:1]} :
{1'b0, ~a};
assign result = extended[7:0];
assign carry_out = extended[8];
assign zero = (result == 8'd0);
assign negative = result[7];
assign overflow = (a[7] ^ result[7]) & ~(a[7] ^ b[7]);
endmodule
Notice the one-to-one correspondence between the RHDL source and the generated Verilog. The case_select becomes a ternary chain, port declarations match exactly, and flag logic is preserved verbatim. CIRCT performs no unnecessary transformations at this stage -- the output is human-readable and debuggable.
| RHDL Construct | Verilog Output |
|---|---|
input :a, width: 8 | input wire [7:0] a |
case_select(op, {...}) | Ternary MUX chain |
a.zero_extend(9) | {1'b0, a} |
result.eq?(0) | (result == 8'd0) |
result[7] | result[7] |
concat(a, carry_in) | {a, carry_in} |
After Verilog generation, CIRCT can further lower the design to a gate-level netlist suitable for FPGA or ASIC synthesis. Here is what the ALU looks like after technology mapping to basic logic gates.
| Metric | Value |
|---|---|
| Total gate count | ~187 equivalent gates |
| AND gates | 42 |
| OR gates | 38 |
| XOR gates | 24 |
| NOT gates | 19 |
| MUX cells (2:1) | 64 |
| Critical path delay | 8 gate levels (ADD path) |
| Estimated max frequency | ~450 MHz (7nm process) |
The gate-level netlist shows how the 8:1 MUX tree for operation selection is decomposed into a cascade of 2:1 MUX cells. The adder chain uses a ripple-carry architecture by default, though CIRCT's optimization passes can substitute carry-lookahead for wider datapaths.
// Gate-level netlist generated by CIRCT
// Technology: generic standard cell library
module ALU_gates (
input wire [7:0] a, b,
input wire [2:0] op,
input wire carry_in,
output wire [7:0] result,
output wire carry_out, zero, negative, overflow
);
// Adder chain (bit 0)
wire sum0, c0;
XOR2 u_xor0 (.A(a[0]), .B(b[0]), .Y(sum0_pre));
XOR2 u_xor1 (.A(sum0_pre), .B(carry_in), .Y(sum0));
AND2 u_and0 (.A(a[0]), .B(b[0]), .Y(g0));
AND2 u_and1 (.A(sum0_pre), .B(carry_in), .Y(p0));
OR2 u_or0 (.A(g0), .B(p0), .Y(c0));
// ... bits 1-7 follow the same pattern
// Operation MUX tree (bit 0)
MUX2 u_mux0_01 (.A(sum0), .B(diff0), .S(op[0]), .Y(sel01_0));
MUX2 u_mux0_23 (.A(and0), .B(or0), .S(op[0]), .Y(sel23_0));
MUX2 u_mux0_lo (.A(sel01_0), .B(sel23_0), .S(op[1]), .Y(sel_lo_0));
// ... high MUX and final select on op[2]
// Zero flag: 8-input NOR
NOR8 u_zero (.A(result), .Y(zero));
endmodule
The critical path runs through the adder's carry chain (8 gate levels for an 8-bit ripple-carry adder), followed by the 3-level MUX tree for operation selection. This gives a total worst-case path of 11 gate levels. For higher performance, CIRCT's optimization passes can break the carry chain using carry-lookahead or carry-select architectures, reducing the critical path to roughly 6 gate levels.
RHDL includes a built-in test harness that lets you verify hardware behavior using familiar Ruby testing patterns. Tests drive inputs, advance simulation, and assert on outputs. The same tests run against both the Ruby simulation model and the generated Verilog, guaranteeing equivalence.
require 'rhdl/test'
class ALUTest < RHDL::Test::ComponentTest
component ALU
test "addition produces correct sum" do
drive :a => 0x3A, :b => 0x15,
:op => ALU::OP_ADD, :carry_in => 0
assert_output :result => 0x4F
assert_output :carry_out => 0
assert_output :zero => 0
assert_output :negative => 0
end
test "addition with carry overflow" do
drive :a => 0xFF, :b => 0x01,
:op => ALU::OP_ADD, :carry_in => 0
assert_output :result => 0x00
assert_output :carry_out => 1
assert_output :zero => 1
end
test "subtraction sets negative flag" do
drive :a => 0x10, :b => 0x20,
:op => ALU::OP_SUB, :carry_in => 1
assert_output :result => 0xF0
assert_output :negative => 1
assert_output :carry_out => 0 # borrow occurred
end
test "bitwise AND masks correctly" do
drive :a => 0b11001010, :b => 0b10101010,
:op => ALU::OP_AND, :carry_in => 0
assert_output :result => 0b10001010
end
test "shift left through carry" do
drive :a => 0b10000001,
:op => ALU::OP_SHL, :carry_in => 1
assert_output :result => 0b00000011
assert_output :carry_out => 1 # MSB shifted into carry
end
test "exhaustive verification of all operations" do
# Test all 256x256 input combinations for ADD
(0..255).each do |a_val|
(0..255).each do |b_val|
drive :a => a_val, :b => b_val,
:op => ALU::OP_ADD, :carry_in => 0
expected = (a_val + b_val) & 0xFF
assert_output :result => expected
end
end
end
end
The exhaustive test above exercises all 65,536 input combinations for the ADD operation, completing in under 2 seconds in the RHDL simulator. This brute-force approach is practical for small combinational blocks. For larger designs, RHDL supports constrained random testing and formal verification through CIRCT's built-in equivalence checking passes.