Back to components

Combinational Components

Direct input-to-output mapping with expressions, case statements, and boolean logic. Pure functions in hardware.

Expressions Case Select Boolean Logic Mux

How Combinational Logic Works

Combinational logic is the foundation of all digital circuits. Unlike sequential logic, combinational components have no memory and no clock. The outputs are determined entirely by the current values of the inputs. When any input changes, the outputs update immediately (after propagation delay). This makes combinational components behave like pure functions: the same inputs always produce the same outputs.

In RHDL, combinational logic is expressed using the behavior block. Every signal assignment inside a behavior block creates a direct wire from inputs to outputs with no implicit state. This maps directly to hardware constructs like logic gates, multiplexers, and lookup tables in an FPGA.

PropertyDescription
ClockNot required. Outputs change whenever inputs change.
StateNone. No memory between evaluations.
PropagationOutputs reflect inputs after gate delay (combinational path).
Use casesALUs, decoders, multiplexers, encoders, comparators
RHDL keywordbehavior

Expression Types

RHDL provides a rich set of operators for building combinational expressions. These map directly to hardware operations and can be freely composed to create complex logic.

Arithmetic and logical operators perform bitwise and mathematical operations on signals. All operations are defined on fixed-width bit vectors, and overflow behavior matches hardware semantics (wrap-around).

arithmetic_ops.rb
class ArithmeticOps < RHDL::Sim::Component
  input  :a, :b, width: 8
  output :sum,  width: 8
  output :diff, width: 8
  output :prod, width: 16
  output :band, :bor, :bxor, width: 8

  behavior do
    # Arithmetic operations
    sum  <= a + b
    diff <= a - b
    prod <= a * b

    # Bitwise logical operations
    band <= a & b
    bor  <= a | b
    bxor <= a ^ b
  end
end

Comparison and shift operations produce boolean results or shifted bit patterns. These are essential building blocks for control logic, priority encoders, and barrel shifters.

compare_shift.rb
class CompareShift < RHDL::Sim::Component
  input  :a, :b,   width: 8
  input  :shamt,    width: 3
  output :eq, :lt, :gt
  output :shl, :shr, :sra, width: 8

  behavior do
    # Comparison operators
    eq <= a.eq?(b)
    lt <= a.lt?(b)
    gt <= a.gt?(b)

    # Shift operations
    shl <= a << shamt  # logical left
    shr <= a >> shamt  # logical right
    sra <= a.sra(shamt) # arithmetic right
  end
end

Case Statements

The case_select construct is RHDL's way of expressing multiplexer-like selection logic. It maps a selector value to one of several output expressions, similar to a case/switch statement in software but synthesized as parallel hardware. Every case_select should include a default value to ensure all paths are covered, preventing unintentional latch inference.

instruction_decoder.rb
class InstructionDecoder < RHDL::Sim::Component
  input  :opcode,     width: 4
  output :alu_ctrl,   width: 3
  output :reg_write
  output :mem_enable
  output :branch

  behavior do
    # Decode ALU control from opcode
    alu_ctrl <= case_select(opcode, {
      0b0000 => 0b000,  # ADD
      0b0001 => 0b001,  # SUB
      0b0010 => 0b010,  # AND
      0b0011 => 0b011,  # OR
      0b0100 => 0b100,  # XOR
      0b0101 => 0b101,  # SLT
    }, default: 0b000)

    # Control signals with boolean case
    reg_write  <= opcode.lt?(0b1000)
    mem_enable <= opcode.eq?(0b1000) | opcode.eq?(0b1001)
    branch     <= opcode[3] & opcode[1]
  end
end

Building an ALU

An Arithmetic Logic Unit is the quintessential combinational component. It takes two data inputs and an operation selector, then produces a result and status flags. This example shows a complete 8-bit ALU with eight operations and four condition flags.

alu.rb
class ALU < RHDL::Sim::Component
  input  :a, :b,     width: 8
  input  :op,        width: 3
  output :result,    width: 8
  output :zero, :carry, :negative, :overflow

  # Operation constants
  OP_ADD = 0b000
  OP_SUB = 0b001
  OP_AND = 0b010
  OP_OR  = 0b011
  OP_XOR = 0b100
  OP_SHL = 0b101
  OP_SHR = 0b110
  OP_SLT = 0b111

  behavior do
    # Extended result for carry detection
    wide_sum  = a.zero_ext(9) + b.zero_ext(9)
    wide_diff = a.zero_ext(9) - b.zero_ext(9)

    # Select operation result
    result <= case_select(op, {
      OP_ADD => a + b,
      OP_SUB => a - b,
      OP_AND => a & b,
      OP_OR  => a | b,
      OP_XOR => a ^ b,
      OP_SHL => a << b[2..0],
      OP_SHR => a >> b[2..0],
      OP_SLT => cond(a.lt?(b), 1, 0),
    })

    # Condition flags
    zero     <= result.eq?(0)
    negative <= result[7]
    carry    <= cond(op.eq?(OP_ADD), wide_sum[8],
               cond(op.eq?(OP_SUB), wide_diff[8], 0))
    overflow <= cond(op.eq?(OP_ADD),
                 (a[7].eq?(b[7])) & (a[7].neq?(result[7])),
               cond(op.eq?(OP_SUB),
                 (a[7].neq?(b[7])) & (a[7].neq?(result[7])),
                 0))
  end
end