Skip to content

RTL Design Patterns · Chapter 5 · Arithmetic & Datapath Patterns

Datapath + Control Separation

A single cycle rarely finishes the job. A multiply-accumulate, a running checksum, or a bus transaction is a sequence of single-cycle operations, and something must decide which operation runs each cycle and when the whole thing is done. Bolting that sequencing onto the compute logic until neither can be understood alone is the disastrous way to build it. The professional way is to split. Put everything that can compute and store into a datapath that makes no decisions, and put every decision into a controller FSM that does no arithmetic, joined by a narrow interface: control signals down, status flags up. This split is what makes a design reusable, verifiable, and easy to grow. This lesson builds a width-generic multiply-accumulate engine, verifies it computes a times b plus c, and studies the shared-resource timing bug that defines the pattern, in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsDatapathControlController FSMMultiply-AccumulateResource Sharing

Chapter 5 · Section 5.6 · Arithmetic & Datapath Patterns

1. The Engineering Problem

You have just finished the ALU of 5.5 — a compute core that, given two operands and an opcode, produces any one of add, subtract, AND, shift, or compare in a single cycle. Now the block above you asks for something the ALU cannot do alone: a multiply-accumulate, acc = a*b + c, on a target that has no hardware multiplier. You already know the trick — a multiply by repeated addition: start the accumulator at c, then add a to it b times. That is b+1 cycles of work, a sequence, and the ALU has no concept of a sequence. It does one operation and forgets. Something else has to decide, each cycle, which operation runs, which register captures the result, and when the loop is finished.

The tempting move is to grow the ALU: give it a little state, an internal counter, a mode bit, an always block that both computes the sum and decides whether to keep looping. Do that a few times — add a running-checksum mode here, a saturating variant there — and you have built a monolith: one block that both computes and sequences, where the arithmetic and the decisions are tangled in the same statements. You cannot reuse the arithmetic without dragging the sequencing along; you cannot change the schedule without risking the math; and you cannot verify either in isolation, because there is no isolation left. When a bug appears you cannot even say whether it is a compute bug or a control bug, because the design does not separate the two.

This is the same crossroads Chapter 4 reached at 4.6: the moment a design needs more than one cycle, it stops being a circuit and becomes a controller driving a datapath. The discipline that keeps it maintainable, reusable, and verifiable is to split the design along that seam — everything that computes into a datapath, every decision into a controller — and connect them across one deliberately narrow interface. That split is the subject of this page, and the multiply-accumulate is the worked example we carry through it.

the-need.v — a sequence the ALU alone cannot express
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Goal: acc = a*b + c, with NO hardware multiplier.
   // Method: acc starts at c, then add a, b times  ->  a SEQUENCE of ALU ops.
   //
   //   cycle 0 : acc <- c            (load)
   //   cycle 1 : acc <- acc + a      (ALU add)     cnt <- b-1
   //   cycle 2 : acc <- acc + a      (ALU add)     cnt <- cnt-1
   //     ...    repeat while (cnt != 0)            <- a DECISION, per cycle
   //   cycle k : cnt == 0            (ZERO status) -> done
   //
   // The ALU can do ONE add per cycle. It cannot decide "keep looping" or
   // "we are done" — that is sequencing. WHO decides which op runs this cycle,
   // which register captures it, and when to stop?  That is the CONTROLLER,
   // and the clean way to add it is to SPLIT compute from control.

2. Mental Model

3. Pattern Anatomy

The split is two boxes and one interface. Everything on this page is a way of respecting that structure.

The datapath (the lower box). A collection of storage and compute that can perform any single-cycle operation on demand and never decides which: the accumulator acc and the loop counter cnt (registers with enables, from 2.x), the shared ALU of 5.5 (add/sub/pass), and the operand muxes (from 1.1) that steer which sources reach the ALU inputs this cycle. Its inputs are control signals; its outputs are the datapath results held in registers plus a bundle of status flags — here zero (is cnt zero?), but in general also less-than, equal, overflow, negative. The datapath is combinational-plus-registers with a clean cut: the ALU and muxes are combinational, the acc/cnt captures are clocked. Crucially it is WIDTH-generic — it computes on WIDTH-bit data and does not know or care what sequence it is part of.

The controller (the upper box). A pure FSM — state register, next-state logic, output logic — from Chapter 4. Its inputs are start and the datapath's status flags; its outputs are the control signals that drive the datapath (alu_op, sel_a/sel_b, en_acc, en_cnt, load) and done. It contains no data arithmetic: it does not compute acc, it commands the datapath to. It reads zero to decide whether the loop continues, and it emits the control word that makes the next single-cycle operation happen.

The interface — control down, status up. This is the load-bearing part.

  • Control signals DOWN. The opcode (alu_op), the mux selects (sel_a, sel_b), the register enables (en_acc, en_cnt), load. Each is a command: do this single-cycle operation now.
  • Status flags UP. zero, and in the general case less-than, equal, overflow, done. Each is a fact the controller branches on: the datapath is in this condition.

Datapath + Control — two boxes, one narrow interface (control down, status up)

data flow
Datapath + Control — two boxes, one narrow interface (control down, status up)commandsdriveproducebranch onCONTROLLER (FSM)state · next-state · output logic — no arithmeticcontrol signals ↓alu_op, sel_a, sel_b, en_acc, en_cnt, load, doneDATAPATHacc + cnt regs · shared ALU · operand muxesstatus flags ↑zero, less-than, overflow, done
The controller sits above the datapath and drives it through control signals going DOWN (opcode, mux selects, register enables, load); the datapath sits below and returns status flags going UP (zero, less-than, overflow, done). The controller makes decisions and no arithmetic; the datapath does arithmetic and makes no decisions. Keep this interface narrow and explicit and the two halves are independent — reuse one datapath under many controllers, verify each half alone. This is language-independent: the same two boxes and one interface exist in SystemVerilog, Verilog, and VHDL.

Why split — four payoffs. The separation is not tidiness for its own sake; each property below is a concrete engineering win.

  • Reuse. One datapath, many controllers. The same accumulator-plus-ALU-plus-muxes that does a multiply-accumulate can, under a different controller, do a running sum, a dot-product, or a divide-by-subtraction — or be driven by a microcode ROM instead of an FSM. The datapath is the reusable asset; the controller is the cheap, swappable part.
  • Verifiability. You can verify the datapath exhaustively — for each control-signal combination, does it produce the right result and flags? — because it has no sequence to get lost in. You verify the controller as a sequence — does it emit the right control word and branch correctly on status? — with the datapath stubbed or modelled. Then you verify the composition. Three tractable problems instead of one intractable one.
  • Scalability. Widen the datapath from 8 to 32 bits and the controller is untouched (control is width-agnostic). Add an operation to the datapath and only the controller states that use it change. The seam localizes change.
  • Timing. With the split explicit you can reason about the interface's timing: is a status combinational (available the same cycle) or registered (available the next cycle)? The controller must sample it at the right time — exactly the 4.6 lesson. A monolith hides this; the split forces you to name it.

The worked micro-example. The datapath is acc (accumulator) and cnt (down-counter) registers, the shared ALU, and operand muxes; the controller sequences acc = a*b + c as: load acc <- c and cnt <- b; then loop — while cnt != 0, do acc <- acc + a and cnt <- cnt - 1; when zero (cnt reached 0), assert done. With a=5, b=3, c=2 the golden result is 5 + 5 + 5 + 2 = 17. Every RTL block below implements one of these three things — the datapath, the controller, or the composition — and every testbench runs this example to done and asserts acc == 17.

4. Real RTL Implementation

This is the core of the page. We build three things — (a) the datapath (acc + cnt registers, shared ALU, operand muxes, driven by control, producing the zero status; WIDTH-generic), (b) the controller FSM (sequences the MAC, branches on zero), and (c) the composed datapath+controller plus the timing/contention bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The worked example a*b + c with a=5, b=3, c=2 → 17 runs end-to-end in all three.

Two disciplines run through every block. First, the datapath makes no decisions — its combinational ALU/mux logic is total (default-assigned, latch-free, the 1.1/5.5 discipline) and its registers use explicit clocked enables (<=, the 2.x discipline). Second, the controller does no arithmetic — it is a pure FSM whose only job is to emit the control word and branch on status.

4a. The datapath — compute + store, no decisions, WIDTH-generic

The datapath holds acc and cnt, owns the single shared ALU, and steers its operands with muxes. Every action it takes is commanded: alu_op picks add/sub/pass, sel_a/sel_b pick the operands, en_acc/en_cnt/load say which register captures. It emits one status flag, zero (is cnt == 0). It decides nothing.

mac_datapath.sv — accumulator + counter + shared ALU + operand muxes (control in, status out)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_datapath #(
       parameter int WIDTH = 8
   )(
       input  logic             clk,
       input  logic             rst,          // synchronous, active-high
       // operands (external)
       input  logic [WIDTH-1:0] a,            // the addend
       input  logic [WIDTH-1:0] b,            // the multiplier (loop count)
       input  logic [WIDTH-1:0] c,            // the accumulate seed
       // CONTROL SIGNALS DOWN (from the controller) — commands, no decisions here
       input  logic             load,         // load acc<-c, cnt<-b this cycle
       input  logic             en_acc,       // capture ALU result into acc
       input  logic             en_cnt,       // capture ALU result into cnt
       input  logic             alu_op,       // 0 = A+B (add), 1 = A-B (sub)
       input  logic             sel_a,        // ALU operand-A mux: 0=acc, 1=cnt
       input  logic             sel_b,        // ALU operand-B mux: 0=a,   1=one
       // DATAPATH OUTPUTS
       output logic [WIDTH-1:0] acc,          // the running result
       // STATUS FLAGS UP (to the controller)
       output logic             zero          // cnt == 0  (loop-terminal)
   );
       logic [WIDTH-1:0] cnt;
       logic [WIDTH-1:0] op_a, op_b, alu_y;
 
       // --- operand muxes (1.1): steer sources to the shared ALU. Combinational,
       //     total (every select drives an output) => latch-free by construction. ---
       assign op_a = sel_a ? cnt : acc;                 // A = acc (accumulate) or cnt (decrement)
       assign op_b = sel_b ? {{(WIDTH-1){1'b0}}, 1'b1}  // B = 1 (for cnt-1)
                           : a;                          //   or a (for acc+a)
 
       // --- the SINGLE shared ALU (5.5): one adder/subtractor, add OR sub, no decision. ---
       assign alu_y = alu_op ? (op_a - op_b) : (op_a + op_b);
 
       // --- registers with clocked enables (2.x): acc and cnt capture on command. ---
       always_ff @(posedge clk) begin
           if (rst) begin
               acc <= '0;
               cnt <= '0;
           end else if (load) begin
               acc <= c;                                 // seed the accumulate
               cnt <= b;                                 // seed the loop count
           end else begin
               if (en_acc) acc <= alu_y;                 // acc <- acc + a
               if (en_cnt) cnt <= alu_y;                 // cnt <- cnt - 1
           end
       end
 
       // --- status flag UP: purely a fact about the datapath, no branching. ---
       assign zero = (cnt == '0);
   endmodule

The datapath is exhaustively describable: for each (load, en_acc, en_cnt, alu_op, sel_a, sel_b) command, the next acc/cnt and the zero flag are fully determined — no sequence, no decision. The testbench drives it directly, with no controller, to prove the compute contract in isolation: command acc + a and cnt - 1 and check the registers step correctly and zero rises when cnt hits 0.

mac_datapath_tb.sv — drive control DIRECTLY (no controller); check acc/cnt step and zero
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_datapath_tb;
       localparam int WIDTH = 8;
       logic clk = 0, rst;
       logic [WIDTH-1:0] a, b, c, acc;
       logic load, en_acc, en_cnt, alu_op, sel_a, sel_b, zero;
       int   errors = 0;
       always #5 clk = ~clk;
 
       mac_datapath #(.WIDTH(WIDTH)) dut (.*);
 
       task automatic step; @(posedge clk); #1; endtask
 
       initial begin
           rst = 1; {load,en_acc,en_cnt,alu_op,sel_a,sel_b} = '0;
           a = 8'd5; b = 8'd3; c = 8'd2;
           step; rst = 0;
 
           load = 1; step; load = 0;                         // acc<-2, cnt<-3
           assert (acc === 8'd2 && zero === 1'b0) else begin $error("load"); errors++; end
 
           // one loop pass, done as TWO datapath commands (add, then dec):
           sel_a=0; sel_b=0; alu_op=0; en_acc=1; step; en_acc=0;   // acc <- acc + a = 7
           assert (acc === 8'd7) else begin $error("acc+a"); errors++; end
           sel_a=1; sel_b=1; alu_op=1; en_cnt=1; step; en_cnt=0;   // cnt <- cnt - 1 = 2
           assert (zero === 1'b0) else begin $error("cnt-1"); errors++; end
 
           if (errors == 0) $display("PASS: datapath obeys control, zero tracks cnt");
           else             $display("FAIL: %0d datapath errors", errors);
           $finish;
       end
   endmodule

The Verilog datapath is the same structure with reg/wire typing and an always @(posedge clk). The muxes are continuous assigns (latch-proof), the ALU is one shared adder/subtractor, and the enables gate the register captures.

mac_datapath.v — same datapath in Verilog (shared ALU, operand muxes, enabled registers)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_datapath #(
       parameter WIDTH = 8
   )(
       input  wire              clk,
       input  wire              rst,
       input  wire [WIDTH-1:0]  a, b, c,
       input  wire              load, en_acc, en_cnt, alu_op, sel_a, sel_b,
       output reg  [WIDTH-1:0]  acc,
       output wire              zero
   );
       reg  [WIDTH-1:0] cnt;
       wire [WIDTH-1:0] op_a, op_b, alu_y;
 
       // operand muxes: total continuous assigns, no latch
       assign op_a = sel_a ? cnt : acc;
       assign op_b = sel_b ? {{(WIDTH-1){1'b0}}, 1'b1} : a;   // 1 or a
       // single shared ALU: add or subtract
       assign alu_y = alu_op ? (op_a - op_b) : (op_a + op_b);
 
       always @(posedge clk) begin
           if (rst) begin
               acc <= {WIDTH{1'b0}};
               cnt <= {WIDTH{1'b0}};
           end else if (load) begin
               acc <= c;
               cnt <= b;
           end else begin
               if (en_acc) acc <= alu_y;
               if (en_cnt) cnt <= alu_y;
           end
       end
 
       assign zero = (cnt == {WIDTH{1'b0}});                 // status UP
   endmodule
mac_datapath_tb.v — self-checking; drive control directly, check acc/cnt/zero (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_datapath_tb;
       parameter WIDTH = 8;
       reg  clk, rst;
       reg  [WIDTH-1:0] a, b, c;
       reg  load, en_acc, en_cnt, alu_op, sel_a, sel_b;
       wire [WIDTH-1:0] acc;
       wire zero;
       integer errors;
       initial clk = 0;
       always #5 clk = ~clk;
 
       mac_datapath #(.WIDTH(WIDTH)) dut (
           .clk(clk), .rst(rst), .a(a), .b(b), .c(c),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt),
           .alu_op(alu_op), .sel_a(sel_a), .sel_b(sel_b), .acc(acc), .zero(zero));
 
       initial begin
           errors = 0;
           rst = 1; load=0; en_acc=0; en_cnt=0; alu_op=0; sel_a=0; sel_b=0;
           a = 8'd5; b = 8'd3; c = 8'd2;
           @(posedge clk); #1; rst = 0;
 
           load = 1; @(posedge clk); #1; load = 0;               // acc<-2 cnt<-3
           if (acc !== 8'd2 || zero !== 1'b0) begin $display("FAIL: load"); errors=errors+1; end
 
           sel_a=0; sel_b=0; alu_op=0; en_acc=1; @(posedge clk); #1; en_acc=0;   // acc<-7
           if (acc !== 8'd7) begin $display("FAIL: acc+a"); errors=errors+1; end
           sel_a=1; sel_b=1; alu_op=1; en_cnt=1; @(posedge clk); #1; en_cnt=0;   // cnt<-2
           if (zero !== 1'b0) begin $display("FAIL: cnt-1"); errors=errors+1; end
 
           if (errors == 0) $display("PASS: datapath obeys control, zero tracks cnt");
           else             $display("FAIL: %0d datapath errors", errors);
           $finish;
       end
   endmodule

In VHDL the datapath is a numeric_std design: acc/cnt are unsigned, the muxes are when/else concurrent assignments, the shared ALU is one add/subtract expression, and the clocked process captures under the enables. zero is a concurrent status output.

mac_datapath.vhd — datapath in VHDL (numeric_std, shared ALU, enabled registers, status out)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_datapath is
       generic ( WIDTH : positive := 8 );
       port (
           clk    : in  std_logic;
           rst    : in  std_logic;                                   -- sync, active-high
           a, b, c : in unsigned(WIDTH-1 downto 0);
           -- CONTROL DOWN
           load   : in  std_logic;
           en_acc : in  std_logic;
           en_cnt : in  std_logic;
           alu_op : in  std_logic;                                   -- '0'=add '1'=sub
           sel_a  : in  std_logic;                                   -- '0'=acc '1'=cnt
           sel_b  : in  std_logic;                                   -- '0'=a   '1'=one
           acc    : out unsigned(WIDTH-1 downto 0);
           -- STATUS UP
           zero   : out std_logic
       );
   end entity;
 
   architecture rtl of mac_datapath is
       signal acc_r, cnt_r : unsigned(WIDTH-1 downto 0);
       signal op_a, op_b, alu_y : unsigned(WIDTH-1 downto 0);
       constant ONE : unsigned(WIDTH-1 downto 0) := to_unsigned(1, WIDTH);
   begin
       -- operand muxes (total when/else, no latch)
       op_a  <= cnt_r when sel_a = '1' else acc_r;
       op_b  <= ONE   when sel_b = '1' else a;
       -- single shared ALU
       alu_y <= (op_a - op_b) when alu_op = '1' else (op_a + op_b);
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   acc_r <= (others => '0');
                   cnt_r <= (others => '0');
               elsif load = '1' then
                   acc_r <= c;
                   cnt_r <= b;
               else
                   if en_acc = '1' then acc_r <= alu_y; end if;
                   if en_cnt = '1' then cnt_r <= alu_y; end if;
               end if;
           end if;
       end process;
 
       acc  <= acc_r;
       zero <= '1' when cnt_r = 0 else '0';                          -- status UP
   end architecture;
mac_datapath_tb.vhd — self-checking; drive control directly (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_datapath_tb is
   end entity;
 
   architecture sim of mac_datapath_tb is
       constant WIDTH : positive := 8;
       signal clk : std_logic := '0';
       signal rst, load, en_acc, en_cnt, alu_op, sel_a, sel_b, zero : std_logic := '0';
       signal a, b, c, acc : unsigned(WIDTH-1 downto 0);
   begin
       dut : entity work.mac_datapath
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, a => a, b => b, c => c,
                     load => load, en_acc => en_acc, en_cnt => en_cnt,
                     alu_op => alu_op, sel_a => sel_a, sel_b => sel_b,
                     acc => acc, zero => zero);
 
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           a <= to_unsigned(5, WIDTH); b <= to_unsigned(3, WIDTH); c <= to_unsigned(2, WIDTH);
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
 
           load <= '1'; wait until rising_edge(clk); wait for 1 ns; load <= '0';   -- acc<-2 cnt<-3
           assert acc = to_unsigned(2, WIDTH) and zero = '0'
               report "datapath load failed" severity error;
 
           sel_a <= '0'; sel_b <= '0'; alu_op <= '0'; en_acc <= '1';
           wait until rising_edge(clk); wait for 1 ns; en_acc <= '0';              -- acc<-7
           assert acc = to_unsigned(7, WIDTH) report "acc+a failed" severity error;
 
           sel_a <= '1'; sel_b <= '1'; alu_op <= '1'; en_cnt <= '1';
           wait until rising_edge(clk); wait for 1 ns; en_cnt <= '0';              -- cnt<-2
           assert zero = '0' report "cnt-1 failed" severity error;
 
           report "datapath self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The controller — a pure FSM that sequences and branches on status

The controller is the grammar. It holds only state, reads start and the datapath's zero, and emits the control word. It does no arithmetic — it never touches acc or cnt data. States: IDLE (wait for start) → LOAD (seed acc/cnt) → ADD (command acc <- acc + a) → DEC (command cnt <- cnt - 1, then branch on zero) → DONE. Splitting ADD and DEC into two states is deliberate: it schedules the two datapath operations onto the single shared ALU one per cycle — the scheduling contract §7 is all about.

mac_ctrl.sv — the controller FSM (emits control DOWN, branches on ZERO status UP)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_ctrl (
       input  logic clk,
       input  logic rst,
       input  logic start,
       input  logic zero,                    // STATUS UP: cnt == 0
       // CONTROL DOWN
       output logic load,
       output logic en_acc,
       output logic en_cnt,
       output logic alu_op,                  // 0=add (acc+a), 1=sub (cnt-1)
       output logic sel_a,                   // 0=acc, 1=cnt
       output logic sel_b,                   // 0=a,   1=one
       output logic done
   );
       typedef enum logic [2:0] {IDLE, LOAD, ADD, DEC, FIN} state_t;
       state_t state, nstate;
 
       // state register
       always_ff @(posedge clk)
           if (rst) state <= IDLE;
           else     state <= nstate;
 
       // next-state logic — branches on the datapath STATUS, no arithmetic
       always_comb begin
           nstate = state;
           unique case (state)
               IDLE: if (start) nstate = LOAD;
               LOAD:            nstate = (zero) ? FIN : ADD;   // b==0 -> nothing to add
               ADD:             nstate = DEC;                  // one shared-ALU op per cycle
               DEC:             nstate = (zero) ? FIN : ADD;   // sample registered zero, then loop
               FIN:             nstate = FIN;
           endcase
       end
 
       // output logic — the control word for each state (Moore: outputs depend on state)
       always_comb begin
           {load, en_acc, en_cnt, alu_op, sel_a, sel_b, done} = '0;
           unique case (state)
               LOAD: load   = 1'b1;                                    // acc<-c, cnt<-b
               ADD: begin en_acc = 1'b1; alu_op = 1'b0; sel_a = 1'b0; sel_b = 1'b0; end  // acc<-acc+a
               DEC: begin en_cnt = 1'b1; alu_op = 1'b1; sel_a = 1'b1; sel_b = 1'b1; end  // cnt<-cnt-1
               FIN: done    = 1'b1;
               default: ;                                             // IDLE: all controls low
           endcase
       end
   endmodule

The controller is verified as a sequence, independently of real arithmetic: drive start, drive a zero model, and check the emitted control word matches the expected schedule cycle by cycle. This is the second half of the "verify each half alone" payoff.

mac_ctrl_tb.sv — verify the SEQUENCE: control word per state, branch on a modelled zero
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_ctrl_tb;
       logic clk = 0, rst, start, zero;
       logic load, en_acc, en_cnt, alu_op, sel_a, sel_b, done;
       int   errors = 0;
       always #5 clk = ~clk;
 
       mac_ctrl dut (.*);
 
       initial begin
           rst = 1; start = 0; zero = 0;
           @(posedge clk); #1; rst = 0;
           start = 1; @(posedge clk); #1; start = 0;   // IDLE -> LOAD
           @(posedge clk); #1;                          // LOAD -> ADD (zero low)
           assert (en_acc && !en_cnt && !alu_op) else begin $error("ADD control wrong"); errors++; end
           @(posedge clk); #1;                          // ADD -> DEC
           assert (en_cnt && alu_op && sel_a && sel_b) else begin $error("DEC control wrong"); errors++; end
           zero = 1; @(posedge clk); #1;                // model cnt hit 0 -> DEC -> FIN
           @(posedge clk); #1;
           assert (done) else begin $error("done not asserted"); errors++; end
           if (errors == 0) $display("PASS: controller emits the correct control sequence");
           else             $display("FAIL: %0d controller errors", errors);
           $finish;
       end
   endmodule

The Verilog controller uses localparam state encodings and two always @(*) blocks (next-state and output) plus a clocked state register — the classic three-block FSM of Chapter 4.

mac_ctrl.v — controller FSM in Verilog (three-block: state reg, next-state, output word)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_ctrl (
       input  wire clk, rst, start, zero,
       output reg  load, en_acc, en_cnt, alu_op, sel_a, sel_b, done
   );
       localparam [2:0] IDLE=3'd0, LOAD=3'd1, ADD=3'd2, DEC=3'd3, FIN=3'd4;
       reg [2:0] state, nstate;
 
       always @(posedge clk)
           if (rst) state <= IDLE;
           else     state <= nstate;
 
       always @(*) begin
           nstate = state;
           case (state)
               IDLE: if (start) nstate = LOAD;
               LOAD:            nstate = zero ? FIN : ADD;
               ADD:             nstate = DEC;
               DEC:             nstate = zero ? FIN : ADD;
               FIN:             nstate = FIN;
               default:         nstate = IDLE;
           endcase
       end
 
       always @(*) begin
           {load, en_acc, en_cnt, alu_op, sel_a, sel_b, done} = 7'b0;   // default: all low
           case (state)
               LOAD: load = 1'b1;
               ADD:  begin en_acc = 1'b1; alu_op = 1'b0; sel_a = 1'b0; sel_b = 1'b0; end
               DEC:  begin en_cnt = 1'b1; alu_op = 1'b1; sel_a = 1'b1; sel_b = 1'b1; end
               FIN:  done = 1'b1;
               default: ;
           endcase
       end
   endmodule
mac_ctrl_tb.v — self-checking; check control word per state (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_ctrl_tb;
       reg clk, rst, start, zero;
       wire load, en_acc, en_cnt, alu_op, sel_a, sel_b, done;
       integer errors;
       initial clk = 0;
       always #5 clk = ~clk;
 
       mac_ctrl dut (.clk(clk), .rst(rst), .start(start), .zero(zero),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt), .alu_op(alu_op),
           .sel_a(sel_a), .sel_b(sel_b), .done(done));
 
       initial begin
           errors = 0; rst = 1; start = 0; zero = 0;
           @(posedge clk); #1; rst = 0;
           start = 1; @(posedge clk); #1; start = 0;      // -> LOAD
           @(posedge clk); #1;                             // -> ADD
           if (!(en_acc && !en_cnt && !alu_op)) begin $display("FAIL: ADD control"); errors=errors+1; end
           @(posedge clk); #1;                             // -> DEC
           if (!(en_cnt && alu_op && sel_a && sel_b)) begin $display("FAIL: DEC control"); errors=errors+1; end
           zero = 1; @(posedge clk); #1;                   // -> FIN
           @(posedge clk); #1;
           if (!done) begin $display("FAIL: done"); errors=errors+1; end
           if (errors == 0) $display("PASS: controller emits the correct control sequence");
           else             $display("FAIL: %0d controller errors", errors);
           $finish;
       end
   endmodule

In VHDL the controller is a state enumeration with a clocked state process and combinational next-state/output processes. process (all) drives every control output on every path (default-first) so the output logic is latch-free.

mac_ctrl.vhd — controller FSM in VHDL (state type, next-state + output logic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity mac_ctrl is
       port (
           clk, rst, start, zero : in  std_logic;
           load, en_acc, en_cnt  : out std_logic;
           alu_op, sel_a, sel_b  : out std_logic;
           done                  : out std_logic
       );
   end entity;
 
   architecture rtl of mac_ctrl is
       type state_t is (IDLE, LOAD, ADD, DEC, FIN);
       signal state, nstate : state_t;
   begin
       -- state register
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then state <= IDLE; else state <= nstate; end if;
           end if;
       end process;
 
       -- next-state: branch on the datapath STATUS, no arithmetic
       process (all)
       begin
           nstate <= state;
           case state is
               when IDLE => if start = '1' then nstate <= LOAD; end if;
               when LOAD => if zero = '1' then nstate <= FIN; else nstate <= ADD; end if;
               when ADD  => nstate <= DEC;
               when DEC  => if zero = '1' then nstate <= FIN; else nstate <= ADD; end if;
               when FIN  => nstate <= FIN;
           end case;
       end process;
 
       -- output logic: default-first (latch-free), one control word per state
       process (all)
       begin
           load <= '0'; en_acc <= '0'; en_cnt <= '0';
           alu_op <= '0'; sel_a <= '0'; sel_b <= '0'; done <= '0';
           case state is
               when LOAD => load <= '1';
               when ADD  => en_acc <= '1'; alu_op <= '0'; sel_a <= '0'; sel_b <= '0';
               when DEC  => en_cnt <= '1'; alu_op <= '1'; sel_a <= '1'; sel_b <= '1';
               when FIN  => done <= '1';
               when others => null;
           end case;
       end process;
   end architecture;
mac_ctrl_tb.vhd — self-checking; check control word per state (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity mac_ctrl_tb is
   end entity;
 
   architecture sim of mac_ctrl_tb is
       signal clk : std_logic := '0';
       signal rst, start, zero : std_logic := '0';
       signal load, en_acc, en_cnt, alu_op, sel_a, sel_b, done : std_logic;
   begin
       dut : entity work.mac_ctrl
           port map (clk => clk, rst => rst, start => start, zero => zero,
                     load => load, en_acc => en_acc, en_cnt => en_cnt,
                     alu_op => alu_op, sel_a => sel_a, sel_b => sel_b, done => done);
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           start <= '1'; wait until rising_edge(clk); wait for 1 ns; start <= '0';  -- -> LOAD
           wait until rising_edge(clk); wait for 1 ns;                              -- -> ADD
           assert en_acc = '1' and en_cnt = '0' and alu_op = '0'
               report "ADD control wrong" severity error;
           wait until rising_edge(clk); wait for 1 ns;                              -- -> DEC
           assert en_cnt = '1' and alu_op = '1' and sel_a = '1' and sel_b = '1'
               report "DEC control wrong" severity error;
           zero <= '1'; wait until rising_edge(clk); wait for 1 ns;                 -- -> FIN
           wait until rising_edge(clk); wait for 1 ns;
           assert done = '1' report "done not asserted" severity error;
           report "controller self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The composition — and the timing/contention bug (buggy vs fixed)

Wire the controller's control outputs to the datapath's control inputs and the datapath's zero back to the controller: that is the whole MAC engine. The top runs the worked example to done, and the golden result is acc = 5*3 + 2 = 17. This is where the interface becomes a contract, and where §7's bug lives: if the controller schedules the accumulate-add and the counter-decrement onto the single shared ALU in the same cycle, they collide — only one can use the ALU — and the result is wrong. The buggy controller below tries to do both in one ADD state; the fixed one splits them into ADD then DEC so the one ALU serves one operation per cycle.

mac_top.sv — compose controller + datapath; BUGGY vs FIXED controller schedule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // ---- BUGGY controller: schedules acc+a AND cnt-1 in the SAME cycle onto the
   //      ONE shared ALU. The ALU can produce only ONE result per cycle, so acc and
   //      cnt cannot both be correct: they collide on the shared resource. ----
   module mac_ctrl_bad (
       input  logic clk, rst, start, zero,
       output logic load, en_acc, en_cnt, alu_op, sel_a, sel_b, done
   );
       typedef enum logic [1:0] {IDLE, LOAD, LOOP, FIN} state_t;
       state_t state, nstate;
       always_ff @(posedge clk) if (rst) state <= IDLE; else state <= nstate;
       always_comb begin
           nstate = state;
           unique case (state)
               IDLE: if (start) nstate = LOAD;
               LOAD:            nstate = (zero) ? FIN : LOOP;
               LOOP:            nstate = (zero) ? FIN : LOOP;   // reads zero; loops in ONE state
               FIN:             nstate = FIN;
           endcase
       end
       always_comb begin
           {load,en_acc,en_cnt,alu_op,sel_a,sel_b,done} = '0;
           unique case (state)
               LOAD: load = 1'b1;
               // BUG: en_acc AND en_cnt asserted together — both want the shared ALU
               //      this cycle, but alu_op/sel_* can pick only ONE operation.
               LOOP: begin en_acc = 1'b1; en_cnt = 1'b1; alu_op = 1'b0; sel_a = 1'b0; sel_b = 1'b0; end
               FIN:  done = 1'b1;
               default: ;
           endcase
       end
   endmodule
 
   // ---- FIXED: use mac_ctrl (ADD then DEC) — one shared-ALU op per cycle. ----
   module mac_top #(
       parameter int WIDTH = 8
   )(
       input  logic clk, rst, start,
       input  logic [WIDTH-1:0] a, b, c,
       output logic [WIDTH-1:0] acc,
       output logic done
   );
       logic load, en_acc, en_cnt, alu_op, sel_a, sel_b, zero;
 
       mac_ctrl u_ctrl (.clk(clk), .rst(rst), .start(start), .zero(zero),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt), .alu_op(alu_op),
           .sel_a(sel_a), .sel_b(sel_b), .done(done));
 
       mac_datapath #(.WIDTH(WIDTH)) u_dp (.clk(clk), .rst(rst), .a(a), .b(b), .c(c),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt), .alu_op(alu_op),
           .sel_a(sel_a), .sel_b(sel_b), .acc(acc), .zero(zero));
   endmodule
mac_top_tb.sv — run a*b+c to done; assert acc === 17 (golden)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_top_tb;
       localparam int WIDTH = 8;
       logic clk = 0, rst, start, done;
       logic [WIDTH-1:0] a, b, c, acc;
       always #5 clk = ~clk;
 
       mac_top #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .start(start),
           .a(a), .b(b), .c(c), .acc(acc), .done(done));
 
       initial begin
           rst = 1; start = 0; a = 8'd5; b = 8'd3; c = 8'd2;   // golden = 5*3+2 = 17
           @(posedge clk); #1; rst = 0;
           start = 1; @(posedge clk); #1; start = 0;
           repeat (40) begin @(posedge clk); #1; if (done) break; end
           assert (done && acc === 8'd17)
               $display("PASS: MAC computed a*b+c = %0d (expected 17)", acc);
           else
               $display("FAIL: acc=%0d done=%b (expected 17) — shared-ALU / timing bug", acc, done);
           $finish;
       end
   endmodule

The Verilog composition mirrors it: the buggy controller asserts both enables in one loop state; the fixed top instantiates mac_ctrl (ADD then DEC). The testbench runs to done and checks acc == 17.

mac_top.v — compose in Verilog; BUGGY (both enables one cycle) vs FIXED (ADD then DEC)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY controller: en_acc and en_cnt in the SAME loop cycle -> shared-ALU collision.
   module mac_ctrl_bad (
       input  wire clk, rst, start, zero,
       output reg  load, en_acc, en_cnt, alu_op, sel_a, sel_b, done
   );
       localparam [1:0] IDLE=0, LOAD=1, LOOP=2, FIN=3;
       reg [1:0] state, nstate;
       always @(posedge clk) if (rst) state <= IDLE; else state <= nstate;
       always @(*) begin
           nstate = state;
           case (state)
               IDLE: if (start) nstate = LOAD;
               LOAD:            nstate = zero ? FIN : LOOP;
               LOOP:            nstate = zero ? FIN : LOOP;
               default:         nstate = FIN;
           endcase
       end
       always @(*) begin
           {load,en_acc,en_cnt,alu_op,sel_a,sel_b,done} = 7'b0;
           case (state)
               LOAD: load = 1'b1;
               LOOP: begin en_acc=1'b1; en_cnt=1'b1; alu_op=1'b0; sel_a=1'b0; sel_b=1'b0; end  // BUG
               FIN:  done = 1'b1;
               default: ;
           endcase
       end
   endmodule
 
   // FIXED top: uses mac_ctrl (ADD then DEC) — one shared-ALU op per cycle.
   module mac_top #(
       parameter WIDTH = 8
   )(
       input  wire clk, rst, start,
       input  wire [WIDTH-1:0] a, b, c,
       output wire [WIDTH-1:0] acc,
       output wire done
   );
       wire load, en_acc, en_cnt, alu_op, sel_a, sel_b, zero;
       mac_ctrl u_ctrl (.clk(clk), .rst(rst), .start(start), .zero(zero),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt), .alu_op(alu_op),
           .sel_a(sel_a), .sel_b(sel_b), .done(done));
       mac_datapath #(.WIDTH(WIDTH)) u_dp (.clk(clk), .rst(rst), .a(a), .b(b), .c(c),
           .load(load), .en_acc(en_acc), .en_cnt(en_cnt), .alu_op(alu_op),
           .sel_a(sel_a), .sel_b(sel_b), .acc(acc), .zero(zero));
   endmodule
mac_top_tb.v — self-checking; run to done, check acc == 17 (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mac_top_tb;
       parameter WIDTH = 8;
       reg  clk, rst, start;
       reg  [WIDTH-1:0] a, b, c;
       wire [WIDTH-1:0] acc;
       wire done;
       integer i;
       initial clk = 0;
       always #5 clk = ~clk;
 
       mac_top #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .start(start),
           .a(a), .b(b), .c(c), .acc(acc), .done(done));
 
       initial begin
           rst = 1; start = 0; a = 8'd5; b = 8'd3; c = 8'd2;      // golden = 17
           @(posedge clk); #1; rst = 0;
           start = 1; @(posedge clk); #1; start = 0;
           for (i = 0; i < 40; i = i + 1) begin @(posedge clk); #1; if (done) i = 40; end
           if (done && acc === 8'd17)
               $display("PASS: MAC computed a*b+c = %0d (expected 17)", acc);
           else
               $display("FAIL: acc=%0d done=%b (expected 17) — shared-ALU / timing bug", acc, done);
           $finish;
       end
   endmodule

In VHDL the composition is a structural architecture instantiating the two entities; the buggy controller asserts both enables in one loop state. The testbench runs the worked example to done and asserts acc = 17.

mac_top.vhd — compose in VHDL (structural); FIXED top uses mac_ctrl (ADD then DEC)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_top is
       generic ( WIDTH : positive := 8 );
       port (
           clk, rst, start : in  std_logic;
           a, b, c         : in  unsigned(WIDTH-1 downto 0);
           acc             : out unsigned(WIDTH-1 downto 0);
           done            : out std_logic
       );
   end entity;
 
   architecture rtl of mac_top is
       signal load, en_acc, en_cnt, alu_op, sel_a, sel_b, zero : std_logic;
   begin
       u_ctrl : entity work.mac_ctrl                             -- FIXED: ADD then DEC
           port map (clk => clk, rst => rst, start => start, zero => zero,
                     load => load, en_acc => en_acc, en_cnt => en_cnt,
                     alu_op => alu_op, sel_a => sel_a, sel_b => sel_b, done => done);
       u_dp : entity work.mac_datapath
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, a => a, b => b, c => c,
                     load => load, en_acc => en_acc, en_cnt => en_cnt,
                     alu_op => alu_op, sel_a => sel_a, sel_b => sel_b,
                     acc => acc, zero => zero);
   end architecture;
mac_top_tb.vhd — self-checking; run a*b+c to done, assert acc = 17 (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mac_top_tb is
   end entity;
 
   architecture sim of mac_top_tb is
       constant WIDTH : positive := 8;
       signal clk : std_logic := '0';
       signal rst, start, done : std_logic := '0';
       signal a, b, c, acc : unsigned(WIDTH-1 downto 0);
   begin
       dut : entity work.mac_top
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, start => start,
                     a => a, b => b, c => c, acc => acc, done => done);
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           a <= to_unsigned(5, WIDTH); b <= to_unsigned(3, WIDTH); c <= to_unsigned(2, WIDTH);  -- golden 17
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           start <= '1'; wait until rising_edge(clk); wait for 1 ns; start <= '0';
           for i in 0 to 39 loop
               wait until rising_edge(clk); wait for 1 ns;
               exit when done = '1';
           end loop;
           assert done = '1' and acc = to_unsigned(17, WIDTH)
               report "MAC wrong: acc /= 17 (shared-ALU / timing bug)" severity error;
           report "MAC self-check complete: a*b+c = 17" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: the controller schedules single-cycle operations onto a datapath with real latency and finite ports — assert one operation per shared resource per cycle, and sample each status when it is actually valid, or the interface's timing contract breaks.

5. Verification Strategy

The whole point of the split is that it makes verification tractable: you prove the datapath correct, the controller correct, and the composition correct — three bounded problems instead of one tangled one. The single invariant that captures a correct engine is:

The datapath obeys every control command exactly, the controller emits exactly the right control sequence and branches correctly on status, and their composition computes the worked example to its golden value.

Everything below is that invariant, made checkable, and it maps directly onto the testbenches in §4.

  • Verify the datapath alone — exhaustively over control (self-checking). Drive the control signals directly, with no controller, and for each command combination check the datapath's next state and flags: load seeds acc<-c/cnt<-b; en_acc with the accumulate mux/op gives acc <- acc + a; en_cnt with the decrement mux/op gives cnt <- cnt - 1; and zero rises exactly when cnt reaches 0. Because the datapath has no sequence, this is a complete check of the compute contract. The three mac_datapath testbenches in §4a do exactly this: SV assert (acc === 8'd7), Verilog if (acc !== 8'd7) $display("FAIL…"), VHDL assert acc = to_unsigned(7,WIDTH) report … severity error.
  • Verify the controller alone — as a sequence, on a modelled status. Drive start and a modelled zero (not the real datapath) and check the emitted control word cycle by cycle: IDLE holds all controls low, LOAD asserts load, ADD asserts en_acc/add, DEC asserts en_cnt/sub, and done rises after zero. This proves the grammar — the schedule and the branch — independent of arithmetic. The mac_ctrl testbenches in §4b assert the control word per state.
  • Verify the composition — run the worked example to a golden value. Wire the two together, drive a=5, b=3, c=2, pulse start, wait for done, and assert acc == 17. The mac_top testbenches in §4c do this: assert (done && acc === 8'd17) (SV), if (done && acc === 8'd17) (Verilog), assert done = '1' and acc = to_unsigned(17,WIDTH) (VHDL). The golden value is the end-to-end contract: 5 + 5 + 5 + 2 = 17.
  • The one-operation-per-shared-resource invariant. State it as a property that holds every cycle: at most one command targets the shared ALU per cycleen_acc and en_cnt are never both asserted in the same cycle, because the single ALU produces one result per cycle. The buggy controller of §4c violates it; the fix (ADD then DEC) restores it. A composition testbench should assert this invariant directly, not just the final value, so the collision is caught at its source.
  • Status-timing corner: sample when valid. Decide, then test the decision: is zero combinational (same cycle) or registered (next cycle)? Here cnt is registered, so zero reflects the captured count and the controller samples it in the state after the decrement — the DEC→FIN branch. Point the composition testbench at operands that stress the boundary (b=0, so the loop must be skipped entirely; b=1, a single pass) and confirm the branch fires on the right cycle. Sampling one cycle early is exactly the §7 failure.
  • Parameter-generic and corner operands. Re-run the composition for WIDTH = 8, 16 and for corner operands: b=0 (result must be c with no add), b=1 (one add), and a larger b that exercises many loop passes. A schedule that works for b=3 can still be wrong at b=0 if the LOAD→FIN skip is missing. Expected waveform: on a correct run, start pulses, then each loop pass shows one en_acc cycle followed by one en_cnt cycle (never both high together), acc steps 2 → 7 → 12 → 17, cnt steps 3 → 2 → 1 → 0, zero rises with cnt=0, and done follows — the visual signature of a schedule that respects the shared ALU.

6. Common Mistakes

Control/status timing mismatch — sampling a status a cycle early or late. The interface has a timing dimension the port list does not show: a status is either combinational (valid the same cycle the control was issued) or registered (valid the next cycle). If the controller branches on zero the cycle the decrement is commanded rather than the cycle the count is captured, it reads the stale value and loops one time too many or too few. This is the 4.6 lesson at the composition level: the controller must sample each status in the cycle it is actually valid, which depends on the datapath's real latency (full post-mortem in §7).

Shared-resource contention — two operations, one resource, same cycle. The datapath has finite ports: one shared ALU, one write port on a register, one bus. If the controller schedules two operations that both need that resource in the same cycle — acc <- acc + a and cnt <- cnt - 1 both through the one ALU — they collide, and at most one can be right. The fix is scheduling: serialize the two onto separate cycles (ADD then DEC), or add the resource (a second adder for the counter). The controller's schedule must fit within the datapath's port count.

Leaking datapath decisions into the controller (or control into the datapath). If the controller starts doing arithmetic — computing acc + a itself and just handing the datapath a number — or the datapath starts deciding which operation to run based on data it inspects, the seam is gone: neither half is reusable, and you cannot verify them in isolation. Keep the contract clean: the datapath computes and never decides; the controller decides and never computes. The moment a data value appears in the controller's next-state logic, or a sequencing decision appears in the datapath, the split has leaked.

A monolithic FSM that encodes datapath values in states — state explosion. The temptation is to fold the datapath into the FSM: a state per count value, a state per accumulator value, so the "controller" is really the whole computation unrolled into states. For a MAC that loops b times this needs a state per possible b — the state count explodes with the data width, and the design becomes unsynthesizable and unverifiable. The datapath's job is to hold those values in registers; the controller's states should count only the phases of the algorithm (IDLE/LOAD/ADD/DEC/FIN), never the data.

Widening the datapath but not the counter (or the control). Because the split localizes change, it is easy to widen acc/a/b/c to a new WIDTH and forget that the loop count cnt and the zero compare must track it too, or that a status the controller samples changed timing. Change one side of the interface and re-verify the other: the seam is narrow, but it is still a contract both sides must honour.

7. DebugLab

The MAC that returned the wrong product — a schedule that collided on the shared ALU

The engineering lesson: the datapath/control interface is a contract, and its terms are timing and resource count — control-signal scheduling must respect the datapath's real latency and its finite ports. The tell is a bug that depends on the sequence ("b=1 passes, b=3 fails, sometimes it hangs"): sequence-dependence in a design that is supposed to compute a fixed function means the schedule, not the arithmetic, is wrong — the controller asked the datapath to do more in one cycle than its ports allow, or read a status before the datapath had produced it. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: schedule at most one operation per shared resource per cycle (serialize onto separate states, or add the resource), and sample each status in the cycle it is actually valid (know whether it is combinational or registered, and branch accordingly). Verify by asserting the one-op-per-resource invariant and running the worked example to its golden value.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "split compute from control, respect the interface contract" habit.

Exercise 1 — Draw the two boxes and the interface

For the MAC engine of this page, list (i) every signal that crosses the interface and its direction (control down / status up), and (ii) for each, one sentence on whether it is a command or a fact. Then state which single signal, if it were sampled one cycle early, would cause the loop to run the wrong number of times — and why.

Exercise 2 — Reuse the datapath under a new controller

Keep the mac_datapath from §4a unchanged. Design (in states + control words, no full RTL) a different controller that makes it compute a running sum of N external values streamed in on aacc <- acc + a each cycle a new value is valid, for N values — instead of a MAC. State what changes (the controller) and what does not (the datapath), and explain why that is the reuse payoff of the split.

Exercise 3 — Schedule onto a scarce resource

Your datapath has exactly one ALU and one register write port. You must, in a loop body, do both acc <- acc + a and cnt <- cnt - 1. Give (i) the serialize schedule (states and how many cycles per loop pass) and (ii) the replicate fix (what hardware you add and the cycles-per-pass it buys). Name the throughput-vs-area trade-off each makes, and state the invariant a testbench should assert to catch a schedule that over-subscribes the ALU.

Exercise 4 — Spot the leak

Two engineers implement the MAC. Engineer A's controller next-state logic contains if (acc + a > threshold) …. Engineer B's datapath contains a case that inspects the accumulator value to decide which operation to run. For each, name which direction the interface leaked (control into datapath, or datapath decision into control), what reuse/verifiability property it destroys, and how you would refactor it back to a clean split.

Continue in RTL Design Patterns (forward links unlock as they ship):

  • FSM + Datapath (FSMD) — Chapter 4.6; the flagship this capstone generalizes — a controller driving a datapath across a control/status interface, here raised to a reusable methodology.
  • ALU Construction — Chapter 5.5; the shared compute core this page's datapath is built around and sequences.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the operand muxes that steer sources to the shared ALU, and the latch-free select discipline the datapath relies on.
  • Register Enable / Load — Chapter 2.x; the enabled acc/cnt registers that capture the ALU result on the controller's command.
  • Inferring RAM — Chapter 6.1 (unlocks as it ships); when the datapath's storage grows from registers to a memory the controller sequences addresses into.
  • Register Files — Chapter 6.x (unlocks as it ships); a multi-port datapath store the controller reads and writes across cycles.
  • valid/ready Handshake — Chapter 9.1 (unlocks as it ships); how the controller's start/done grow into a backpressured interface to the block above.

Backward / composed patterns:

  • FSM Fundamentals — Chapter 4.1; the three-block controller (state register + next-state + output logic) this page's controller is.
  • The Register Pattern (D-FF) — Chapter 2.1; the storage atom the accumulator and counter are built from.
  • Adders & Subtractors — Chapter 5.1; the shared add/subtract unit at the heart of the datapath's ALU.
  • Multipliers — Chapter 5.x; the multiply-by-repeated-addition method this page's worked example sequences by hand.
  • Barrel Shifters — Chapter 5.3; another datapath functional unit a controller can sequence across cycles.
  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applies at the block level.
  • What Is an RTL Design Pattern? — Chapter 0.1; why the datapath/control split earns the name "pattern" — a recurring problem, a reusable form, a synthesis reality, and a signature failure.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Case Statements — the construct behind the controller's next-state and output logic and the datapath's operation select.
  • Arithmetic Operators — the +/- the shared ALU is built from, and the width rules the WIDTH-generic datapath depends on.
  • Blocking and Non-Blocking Assignments — why the clocked acc/cnt captures use <= and the combinational control/mux logic uses =.
  • If-Else Statements — the conditional branches the controller uses to sequence on the zero status.

11. Summary

  • Split compute from control. A datapath computes and stores (the ALU, accumulator, operand muxes) and makes no decisions; a controller is a pure FSM that decides which operation runs each cycle and does no arithmetic. The datapath is the verbs and nouns; the controller is the grammar.
  • One narrow interface — control down, status up. Control signals (alu_op, sel_a/sel_b, en_acc/en_cnt, load, done) go down as commands; status flags (zero, less-than, overflow) come up as facts the controller branches on. Keep it narrow and the two halves stay independent.
  • The split pays off four ways. Reuse — one datapath, many controllers (or microcode). Verifiability — exhaust the datapath, sequence-test the controller, then check the composition, three tractable problems. Scalability — widen the datapath, the controller is untouched. Timing — the split forces you to name combinational vs registered status and sample it correctly.
  • The interface is a timing/resource contract. The controller must schedule at most one operation per shared resource per cycle (serialize onto separate states, or add the resource) and sample each status in the cycle it is valid (combinational = same cycle, registered = next). Break either and you get the §7 shared-ALU / timing-contention bug — a wrong answer on exactly the sequences that collide.
  • Verify with a golden value. Drive the datapath exhaustively over control, the controller as a sequence on a modelled status, and the composition end-to-end — running the worked multiply-accumulate a*b + c with a=5, b=3, c=2 to done and asserting acc == 17, in self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.