Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
    • Low-Power RTL Hub
    • DFT and Scan Hub
    • Physical Awareness Hub
    • Waveform Debug Hub
    • STA and ECO Hub
    • RTL-to-Implementation Closure
    • DFT-Ready RTL
    • Capstone: Resilient Streaming Peripheral
    • Principal Design Review
    • Senior RTL Interview Questions & Model Answers
      • Interview: Combinational and Arithmetic
      • Interview: Sequential and Pipelines
      • Interview: FSM and Control
        • Choosing an FSM Encoding from Timing
        • Moore vs Mealy Outputs
        • Proving an FSM Correct
        • The Controller and Datapath Split
      • Interview: Timing and STA
      • Interview: CDC and Reset
      • Interview: Memories and Buses
      • Interview: Low-Power Design
      • Interview: Debug and ECO
      • Interview: Coding and Whiteboard
  • Glossary and Notation
Loading lesson

‹Interview: FSM and Control
  1. Topics
  2. RTL Design
  3. Senior & Interview
  4. Senior RTL Interview Questions & Model Answers
  5. Interview: FSM and Control
  6. The Controller and Datapath Split

Part 9 · Senior · Senior

The Controller and Datapath Split

A GCD controller sequencing subtract micro-ops, checked against a reference over all operands.

On this page

    Why this matters

    A subtractive GCD takes a different number of cycles for each operand pair. Waiting a fixed ten cycles gives the wrong answer for some pairs, and two blocks writing one register becomes a race.

    After this lesson you can: draw the controller and datapath boundary, say who drives each signal, and signal completion instead of assuming a latency.

    The question

    "This operation takes several cycles. How do you split the controller from the datapath?" The interviewer is testing ownership discipline and the command-status contract between the two blocks.

    The complete answer draws the boundary, names who drives what, and shows a multi-cycle run proved against a reference.

    Model answer

    Block diagram: start enters the controller FSM with states IDLE, LOAD, RUN and DONE. The controller sends load, sub_a and sub_b to the datapath holding the a and b registers. The datapath returns a_gt_b and a_eq_b. The controller drives done_o and busy.
    The split: commands go down from the controller, status comes back from the datapath, and only the controller talks to the outside.

    Read the arrows as ownership. The controller has four states: IDLE waits for start and asserts load on the accepting edge, LOAD is one startup cycle, RUN issues one subtract per edge until a_eq_b, and DONE pulses done_o before returning to IDLE.

    The controller issues a command valid for each operation edge. Consecutive commands may keep a signal high across several cycles: each edge performs another micro-operation. The datapath owns the data registers; status flows back combinationally from them.

    Signal

    Direction

    Rule

    load, sub_a, sub_b

    Controller to datapath

    At most one command per edge; a repeated subtract may remain high across consecutive edges

    a_gt_b, a_eq_b

    Datapath to controller

    Combinational from registers; they describe current values

    start, done_o, busy

    Outside to controller

    Start is accepted only in IDLE; operands are captured on that edge; done is a one-cycle pulse

    Exactly one block drives each signal. The controller never touches a data register, and the datapath never decides the sequence. load is asserted combinationally in IDLE when start is high, so the datapath captures operands on the accepting edge. The caller may change them immediately afterwards. LOAD is a startup state, not a later sampling point.

    Multi-cycle work is just repeated micro-ops. The controller sequences subtract steps until the status says equal, then raises done_o. Variable latency is expected, so completion is signalled, not assumed.

    Status comparators sit on the controller critical path, since they feed its next-state choice combinationally. That is the known cost of the split, and STA must cover it.

    Interview

    The FSM commands each operation edge; the datapath owns registers and reports status. This variable-latency operation signals completion; a specified fixed-latency datapath can instead use a counted wait.

    Edge (15,1)

    Pre-edge command

    Post-edge operands/state

    1: accept start in IDLE

    load

    15,1 / LOAD

    2

    None

    15,1 / RUN

    3, 4, …, 16

    sub_a on every edge

    14,1 then 13,1 … then 1,1 / RUN

    17

    None: equality seen

    1,1 / DONE; done_o visible after edge

    18

    None

    Return IDLE; downstream consumes pre-edge done

    Follow-ups

    • One operand is zero. What breaks? The subtract loop never reaches equal, so the machine never finishes. Guard zero up front (b == 0 means done with a), or require nonzero inputs.

    • A datapath step needs two cycles instead of one. What changes? If exactly two cycles is guaranteed, wait that count and qualify result validity. If latency varies, issue once and wait for completion; do not repeat acceptance accidentally while busy.

    • What sets the throughput here? One subtraction per RUN edge, plus acceptance, LOAD and completion/return overhead. Higher overlapping throughput requires an architecture change; a faster clock changes results per second, not subtracts per cycle.

    Common wrong answers

    • "The controller reads the datapath registers directly." That welds the two blocks together. The controller reads only the status flags, so the datapath stays reusable and separately checkable.

    • "Wait a fixed 10 cycles, then take the result." Cycle counting breaks when operands change the run length. Poll done_o instead.

    • "Both blocks can clear the accumulator." Two writers on one register are rejected by always_ff checking, or, with plain always, become an order-dependent simulation race (no X appears) and a multi-driver synthesis error. Ownership is exclusive, always.

    Common pitfall

    Mutually exclusive commands (load, sub_a, sub_b) must never assert together. Datapath priority is not a substitute for checking the controller contract; the bench checks a named command vector on every active edge.

    Check yourself Why does the controller use a_eq_b from the datapath instead of comparing the operands itself?

    The operands live in the datapath. Comparing them in the controller would duplicate the registers or read across the boundary, breaking the split. Status flags are the only way back.

    Check yourself The GCD run for operands 15 and 1 takes 14 subtracts. How does the testbench know when to read the result?

    It waits for done_o with a watchdog, then reads. The run length varies with the operands, so no fixed delay would work for all pairs.

    Testbench

    A subtractive GCD split into datapath, controller, and top wiring, checked against a reference over all 225 nonzero 4-bit operand pairs. The test changes both operand pins to zero immediately after acceptance to check the sampling contract. After each run it waits one cycle for the return to IDLE, since a start arriving during DONE would be missed.

    gcd_datapath.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    module gcd_datapath (
      input  logic       clk, rst_n, load, sub_a, sub_b,
      input  logic [3:0] a_in, b_in,
      output logic [3:0] a, b
    );
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin a <= 4'd0; b <= 4'd0; end
        else if (load)  begin a <= a_in; b <= b_in; end
        else if (sub_a) a <= a - b;
        else if (sub_b) b <= b - a;
    endmodule
    
    module gcd_ctrl (
      input  logic start, a_gt_b, a_eq_b, clk, rst_n,
      output logic load, sub_a, sub_b, busy, done_o
    );
      typedef enum logic [1:0] {IDLE, LOAD, RUN, DONE} state_t;
      state_t state, next_state;
    
      always_comb begin
        next_state = state;
        load = 1'b0; sub_a = 1'b0; sub_b = 1'b0;
        busy = 1'b1; done_o = 1'b0;
        case (state)
          IDLE: begin
            busy = 1'b0;
            if (start) begin load = 1'b1; next_state = LOAD; end
          end
          LOAD: next_state = RUN;              // operands were captured on the accepting IDLE edge
          RUN:  if (a_eq_b)      next_state = DONE;
                else if (a_gt_b) sub_a = 1'b1;
                else             sub_b = 1'b1;
          DONE: begin busy = 1'b0; done_o = 1'b1; next_state = IDLE; end
          default: next_state = IDLE;
        endcase
      end
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) state <= IDLE;
        else        state <= next_state;
    endmodule
    
    module gcd_top (
      input  logic       clk, rst_n, start,
      input  logic [3:0] a_in, b_in,
      output logic       busy, done_o,
      output logic [3:0] result
    );
      logic load, sub_a, sub_b, a_gt_b, a_eq_b;
      logic [3:0] a, b;
      assign a_gt_b = (a > b);
      assign a_eq_b = (a == b);
      assign result = a;
      gcd_datapath dp (.*);
      gcd_ctrl ct (.*);
    endmodule
    

    Remember

    • The controller issues commands and reads only status flags; the datapath owns the data registers.

    • Exactly one block drives each signal, and mutually exclusive commands never assert together.

    • Status comparators sit on the controller's critical path, and STA must cover them.

    Rule of thumb: for variable latency, poll done_o instead of counting cycles.

    Keep going

    • Practice: Iterative divider integration with a start, busy and done protocol

    • Interview: run a mock interview on this topic

    Related topics

    • Signal contract between FSM and datapath

    • The sequencer mindset

    Previous

    324 / 350

    Next: Interview: Timing and STA

    Up next

    Up nextInterview: Timing and STA

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Choosing an FSM Encoding from Timing
    • Moore vs Mealy Outputs