VHDL · Chapter 20.4 · Capstone Projects
Capstone: Pipelined ALU
The fourth capstone moves from interfaces and storage to computation, in the form of a pipelined ALU. It selects an operation by opcode, covering add, subtract, bitwise AND, OR, and XOR, shift, and compare, using the standard numeric arithmetic and parameterized by a width generic. The key architectural element is the pipeline. The work is split into balanced stages, roughly decode, compute, and flags, with a valid bit and aligned side data flowing alongside, so once full it produces one result per cycle at a high clock. It generates the standard condition flags for zero, carry, overflow, and negative, which must be computed correctly for signed and unsigned values alike. Building it applies arithmetic synthesis, pipelining and timing-driven design, and datapath and control structure. Verification checks every opcode against a reference model, with corner values, back-to-back operations, and flag correctness.
Intermediate16 min readVHDLCapstoneALUPipelineArithmeticVerification
1. Engineering intuition — an opcode-selected datapath on an assembly line
An ALU is a bank of operations sharing inputs, with an opcode choosing which result to keep — conceptually a big multiplexer over an adder, a logic block, a shifter, and a comparator. The capstone twist is making it fast by pipelining: rather than computing the whole thing in one deep combinational path, you split it into stages on an assembly line, so a new operation enters every cycle while earlier ones finish — high throughput at a high clock, at the cost of a fixed latency. The discipline that makes a pipeline correct is carrying a valid bit and any side data (like the opcode or a tag) alongside each operation through the stages, so results stay matched to their requests. And the ALU must produce the right flags — zero, carry, overflow, negative — which is where signed-vs-unsigned and width growth bite. Picture an opcode-selected datapath stretched across pipeline registers, and it falls into place.
2. Formal explanation — the pipelined ALU architecture
entity alu is
generic ( WIDTH : positive := 32 );
port ( clk, rst : in std_logic;
in_valid : in std_logic; out_valid : out std_logic;
opcode : in std_logic_vector(3 downto 0);
a, b : in std_logic_vector(WIDTH-1 downto 0);
result : out std_logic_vector(WIDTH-1 downto 0);
zero, carry, overflow, negative : out std_logic );
end entity;
-- OPS (numeric_std): ADD, SUB, AND, OR, XOR, SHL, SHR, CMP (signed/unsigned per opcode).
-- PIPELINE (balanced stages), valid + side data aligned (18.1):
-- STAGE 1 decode : register a, b, opcode, in_valid.
-- STAGE 2 compute : selected op on the registered operands (add uses WIDTH+1 for carry, 16.5).
-- STAGE 3 flags : register result + derive zero/carry/overflow/negative; out_valid follows.
-- FLAGS:
-- zero = (result = 0)
-- carry = WIDTH+1th bit of an unsigned add/sub
-- overflow = signed overflow (operand signs vs result sign)
-- negative = result MSB (signed)The ALU is a width-generic, opcode-selected set of numeric_std operations split across balanced
pipeline stages with a valid bit and aligned side data, producing zero/carry/overflow/negative
flags. Add/subtract use the extra carry bit (WIDTH+1) so the carry/overflow are correct (16.5).
3. Production usage — pipelined compute with aligned valid and flags
process (clk) begin
if rising_edge(clk) then
if rst = '1' then v1 <= '0'; v2 <= '0';
else
-- STAGE 1: decode/register operands + valid (align opcode as side data)
a1 <= a; b1 <= b; op1 <= opcode; v1 <= in_valid;
-- STAGE 2: compute the selected op (extra bit for add/sub carry)
case op1 is
when OP_ADD => sum2 <= std_logic_vector(resize(unsigned(a1),WIDTH+1) + resize(unsigned(b1),WIDTH+1));
when OP_SUB => sum2 <= std_logic_vector(resize(unsigned(a1),WIDTH+1) - resize(unsigned(b1),WIDTH+1));
when OP_AND => sum2 <= '0' & (a1 and b1);
when OP_XOR => sum2 <= '0' & (a1 xor b1);
-- ... OR, shifts, compare ...
when others => sum2 <= (others => '0');
end case;
op2 <= op1; v2 <= v1; -- side data + valid advance with the data
-- STAGE 3: register result + flags
result <= sum2(WIDTH-1 downto 0);
carry <= sum2(WIDTH); -- carry-out from the extra bit
zero <= '1' when sum2(WIDTH-1 downto 0) = (WIDTH-1 downto 0 => '0') else '0';
negative <= sum2(WIDTH-1);
out_valid <= v2; -- valid marks a real result (handles fill/flush)
end if;
end if;
end process;What hardware does this become? A multi-stage datapath: a decode/register stage, a compute stage that is an
opcode-selected mux over an adder (with the extra carry bit), logic, shifter, and comparator, and a flags stage
— each separated by registers so the unit runs at a high clock and delivers one result per cycle once full. The
valid bit and aligned opcode travel with each operation so results never desync from their requests
(18.1), and out_valid is low during fill/flush so downstream logic ignores partial slots. The flags fall out of
the extra-bit arithmetic: carry from bit WIDTH, negative from the MSB, zero from a result compare, and
overflow from the signed sign relationship. Width-generic, it serves an 8-bit or 64-bit datapath from one
source.
4. Structural interpretation — the pipelined ALU stages
5. Simulation interpretation — pipelined results, valid lag, and flags
Pipelined ALU: results appear after the fill latency, one per cycle, with flags
8 cycles6. Debugging example — wrong flags and a desynchronized pipeline
Expected: correct results with correct flags, each matched to its operation. Observed: the carry or overflow flag is wrong (especially at min/max values), and/or under back-to-back operations a result is paired with the wrong opcode's flags or arrives a cycle off. Root cause: the flags were computed without the extra carry bit (so carry/overflow were truncated away, 16.5) or with the wrong signed/unsigned interpretation; and/or the opcode/valid side data was not aligned through the pipeline, so the flags stage used a different operation's opcode than the result it was flagging. Fix: compute add/subtract in WIDTH+1 bits so carry (bit WIDTH) and overflow (signed sign relationship) are correct, choose signed vs unsigned per the opcode, and align the opcode and valid through every pipeline stage so each result is flagged by its own operation. Engineering takeaway: ALU correctness needs the extra carry bit for proper carry/overflow flags and aligned side data so flags and results stay matched to their opcode — truncated arithmetic or misaligned pipeline data produces wrong flags and mispaired results.
-- BUG: WIDTH-bit add drops the carry; opcode not pipelined -> flags belong to a different op.
-- FIX: compute in WIDTH+1 bits (carry = bit WIDTH); advance opcode + valid through every stage.
-- sum2 <= resize(unsigned(a1),WIDTH+1) + resize(unsigned(b1),WIDTH+1); carry <= sum2(WIDTH);
-- op2 <= op1; v2 <= v1; -- side data aligned with the data it describes7. Common mistakes & what to watch for
- Flags without the extra bit. Compute add/subtract in WIDTH+1 bits so carry and overflow are correct; a same-width add truncates them (16.5).
- Wrong signed/unsigned per opcode. Overflow and compare differ for signed vs unsigned; interpret operands by the opcode.
- Unaligned side data. Advance the opcode and valid through every pipeline stage so each result is flagged by its own operation (18.1).
- Forgetting the valid bit. Propagate
out_validso fill/flush slots are not mistaken for results. - Verifying only typical values. Check every opcode against a reference model at corner values (min/max/overflow/carry) and back-to-back, not just a few easy cases.
8. Engineering insight & continuity
The ALU capstone builds verified computation: a width-generic, opcode-selected numeric_std datapath
split into balanced pipeline stages with aligned valid/side data, producing correct zero/carry/overflow/
negative flags via extra-bit arithmetic — proven per-opcode against a reference model across corner values
and back-to-back operations. It is the model for any pipelined arithmetic block. The final two capstones integrate
toward a system: the next lesson, Capstone: Memory Controller, builds a block that sequences external memory
with timing and state, before the closing SoC-integration capstone.