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:
Subtraction reuses the adder: a + ~b + 1: invert and add one is
two's complement in action, so ADD and SUB share hardware, selected by
one bit.
Flags: zero (result all zeros, one big NOR), carry (unsigned
overflow out of bit 8) and negative (MSB). Real ISAs add signed
overflow; try deriving it.
SLT peeks at the subtractor's sign, comparison is subtraction,
which is why CPUs implement < with the ALU they already have.
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.
Try it live
Open this lesson in a playground, edit the code, and re-run it.