Capstone: A Simple ALU

An arithmetic logic unit is where the earlier lessons snap together. It is, structurally, nothing new: a subtract-capable adder (lesson 6), the gates (lesson 1) computing AND/OR/XOR in parallel, and a big mux (lesson 2): the case on op: choosing which result to present. Registers around it (lessons 3–5) would make it a pipeline stage; a state machine (lesson 8) sequencing operations over it would make it a processor's execute unit. That's genuinely how CPUs begin.

The 8-bit ALU below implements eight operations:

000 ADD    001 SUB    010 AND    011 OR
100 XOR    101 SLL    110 SRL    111 SLT (set if less than, signed)

Details worth reading closely:

In the waveform, the testbench walks all eight ops on fixed operands (a=0x5A, b=0x0F), then shows zero firing on SUB with equal inputs. Predict each result before checking, by this lesson you can.

Experiment: add an opcode 111 -> MUL using lesson 7's * (mind the width!), or registered outputs to make it a real pipeline stage.

The design

Verilog, design.v
// 8-bit ALU: 8 ops, shared adder/subtractor, flags.
module alu (
    input  wire [7:0] a, b,
    input  wire [2:0] op,
    output reg  [7:0] result,
    output wire       zero,
    output wire       carry,     // unsigned carry/borrow out of add/sub
    output wire       negative
);
    localparam [2:0] ADD = 3'd0, SUB = 3'd1, AND_ = 3'd2, OR_ = 3'd3,
                     XOR_ = 3'd4, SLL = 3'd5, SRL = 3'd6, SLT = 3'd7;

    // One adder does both ADD and SUB: a + (~b) + 1 is a - b.
    wire       is_sub = (op == SUB) || (op == SLT);
    wire [7:0] b_eff  = is_sub ? ~b : b;
    wire [8:0] addsub = {1'b0, a} + {1'b0, b_eff} + {8'd0, is_sub};

    // Signed less-than from the subtraction's sign, fixed up for overflow.
    wire ovf = (a[7] ^ b_eff[7] ^ 1'b1) & (a[7] ^ addsub[7]);
    wire slt = addsub[7] ^ ovf;

    always @* begin
        case (op)
            ADD, SUB: result = addsub[7:0];
            AND_:     result = a & b;
            OR_:      result = a | b;
            XOR_:     result = a ^ b;
            SLL:      result = a << b[2:0];
            SRL:      result = a >> b[2:0];
            default:  result = {7'd0, slt};   // SLT
        endcase
    end

    assign zero     = (result == 8'd0);
    assign carry    = addsub[8];
    assign negative = result[7];
endmodule
Show the VHDL version
VHDL, design.vhd
-- 8-bit ALU: 8 ops, shared adder/subtractor, flags.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity alu is
    port (
        a, b     : in  std_logic_vector(7 downto 0);
        op       : in  std_logic_vector(2 downto 0);
        result   : out std_logic_vector(7 downto 0);
        zero     : out std_logic;
        carry    : out std_logic;
        negative : out std_logic
    );
end entity;

architecture rtl of alu is
    signal is_sub : std_logic;
    signal b_eff  : std_logic_vector(7 downto 0);
    signal addsub : unsigned(8 downto 0);
    signal ovf, slt : std_logic;
    signal res    : std_logic_vector(7 downto 0);
begin
    is_sub <= '1' when op = "001" or op = "111" else '0';
    b_eff  <= not b when is_sub = '1' else b;
    addsub <= resize(unsigned(a), 9) + resize(unsigned(b_eff), 9)
              + ("00000000" & is_sub);
    ovf <= (not (a(7) xor b_eff(7))) and (a(7) xor addsub(7));
    slt <= addsub(7) xor ovf;

    process (all) begin
        case op is
            when "000" | "001" => res <= std_logic_vector(addsub(7 downto 0));
            when "010" => res <= a and b;
            when "011" => res <= a or b;
            when "100" => res <= a xor b;
            when "101" => res <= std_logic_vector(
                shift_left(unsigned(a), to_integer(unsigned(b(2 downto 0)))));
            when "110" => res <= std_logic_vector(
                shift_right(unsigned(a), to_integer(unsigned(b(2 downto 0)))));
            when others => res <= "0000000" & slt;
        end case;
    end process;

    result   <= res;
    zero     <= '1' when res = x"00" else '0';
    carry    <= addsub(8);
    negative <= res(7);
end architecture;
Show the MyHDL (Python) version
MyHDL, design.py
from myhdl import block, Signal, intbv, always_comb, instance, delay, StopSimulation

@block
def alu(a, b, op, result, zero, carry, negative):
    @always_comb
    def logic():
        is_sub = 1 if (op == 1 or op == 7) else 0          # SUB or SLT
        beff = intbv((~b) & 0xFF)[8:] if is_sub else intbv(int(b))[8:]
        s = intbv(int(a) + int(beff) + is_sub)[9:]         # shared add/sub
        ovf = 1 if ((not (a[7] ^ beff[7])) and (a[7] ^ s[7])) else 0
        slt = 1 if (s[7] ^ ovf) else 0
        res = intbv(0)[8:]
        o = int(op)
        if o == 0 or o == 1:
            res[:] = s[8:0]
        elif o == 2:
            res[:] = a & b
        elif o == 3:
            res[:] = a | b
        elif o == 4:
            res[:] = a ^ b
        elif o == 5:
            res[:] = (int(a) << (int(b) & 7)) & 0xFF
        elif o == 6:
            res[:] = int(a) >> (int(b) & 7)
        else:
            res[:] = slt
        result.next = res
        zero.next = 1 if res == 0 else 0
        carry.next = s[8]
        negative.next = res[7]
    return logic

@block
def tb():
    a = Signal(intbv(0x5A)[8:]); b = Signal(intbv(0x0F)[8:])
    op = Signal(intbv(0)[3:])
    result = Signal(intbv(0)[8:])
    zero, carry, negative = [Signal(bool(0)) for _ in range(3)]
    dut = alu(a, b, op, result, zero, carry, negative)

    @instance
    def stim():
        for i in range(1, 8):                 # walk all 8 ops
            yield delay(10); op.next = i
        yield delay(10); a.next, b.next, op.next = 0x33, 0x33, 1   # zero flag
        yield delay(10); a.next, b.next, op.next = 0xF6, 0x03, 7   # signed <
        yield delay(10); raise StopSimulation
    return dut, stim

inst = tb()
inst.config_sim(trace=True)
inst.run_sim()

The testbench

Verilog, tb.v
`timescale 1ns/1ns
module tb;
    reg  [7:0] a = 8'h5A, b = 8'h0F;
    reg  [2:0] op = 0;
    wire [7:0] result;
    wire zero, carry, negative;

    alu dut (.a(a), .b(b), .op(op), .result(result),
             .zero(zero), .carry(carry), .negative(negative));

    integer i;
    initial begin
        $dumpfile("wave.vcd"); $dumpvars(0, tb);
        // walk all 8 ops on 0x5A, 0x0F
        for (i = 1; i < 8; i = i + 1)
            #10 op = i[2:0];
        // zero flag demo: 0x33 - 0x33
        #10 begin a = 8'h33; b = 8'h33; op = 3'd1; end
        // signed comparison demo: -10 < 3
        #10 begin a = 8'hF6; b = 8'h03; op = 3'd7; end
        #10 $finish;
    end
endmodule
Show the VHDL testbench
VHDL, tb.vhd
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb is end entity;
architecture sim of tb is
  signal a : std_logic_vector(7 downto 0) := x"5A";
  signal b : std_logic_vector(7 downto 0) := x"0F";
  signal op : std_logic_vector(2 downto 0) := "000";
  signal result : std_logic_vector(7 downto 0);
  signal zero, carry, negative : std_logic;
begin
  dut : entity work.alu port map (a=>a, b=>b, op=>op, result=>result,
        zero=>zero, carry=>carry, negative=>negative);
  process begin
    for i in 1 to 7 loop                 -- walk all 8 ops
      wait for 10 ns; op <= std_logic_vector(to_unsigned(i, 3));
    end loop;
    wait for 10 ns; a <= x"33"; b <= x"33"; op <= "001";   -- zero flag
    wait for 10 ns; a <= x"F6"; b <= x"03"; op <= "111";   -- signed <
    wait for 10 ns; std.env.stop;
  end process;
end architecture;

The MyHDL version keeps the design and its testbench in one design.py.

Simulated waveform

This trace was produced by actually simulating the code above with Icarus Verilog.

10 20 30 40 50 60 70 80 90 t (ns) zero result[7:0] 69 4B A 5F 55 0 0 1 negative carry a[7:0] 5A 33 F6 b[7:0] F 33 3 op[2:0] 0 1 2 3 4 5 6 7 1 7 i[31:0] 1 2 3 4 5 6 7 8 is_sub ovf slt b_eff[7:0] F F0 F F0 CC FC addsub[8:0] 69 14B 69 14B 100 1F3 ADD 0 AND_ 2 OR_ 3

Try it live

Open this lesson in a playground, edit the code, and re-run it.

Verilog → VHDL → MyHDL →

Put it to work

Tools that apply what this lesson covers.

Cores to explore

Open IP from the registry that builds on this.