HomeCDC GuideDay 8
DAY 8 · ADVANCED CDC PATTERNS

Control + Data CDC — Arbiters & Bus Crossing

By EcrioniX · Updated Jun 23, 2026

Crossing a single bit across clock domains is hard enough. Now imagine two masters in different clock domains both trying to own a shared bus — who gets it? How does the bus transfer control? And how do you guarantee that when domain B receives a "bus granted" signal, the data from domain A is already fully stable? This is the arbiter-based CDC problem, and it is one of the hardest topics in SoC design.

1. The Problem with Shared Buses Across Domains

Modern SoCs routinely have multiple bus masters: a CPU in one clock domain, a DMA engine in another, a hardware accelerator in a third. These masters share a memory-mapped bus — AHB, AXI, or a custom fabric. The moment you have two masters in different clock domains requesting the same bus, you face a compounded CDC problem:

None of these can be solved by simply synchronizing every signal individually. Independent synchronizers on different signals introduce different latencies, destroying the timing relationship between them. You need an arbitration mechanism that the entire system agrees on, with formal timing guarantees.

The independence problem

If you route data[31:0] and data_valid through separate 2-FF synchronizers, data_valid might arrive 1–2 cycles before or after the data bits settle. The receiver will either miss valid data or latch corrupt data. You cannot synchronize control and data independently — they must be coordinated through an arbiter protocol.

2. Simple Bus Arbiters — Round-Robin and Priority

A bus arbiter receives request signals from multiple masters and asserts a single grant signal to exactly one of them at a time. The two classic schemes are:

SchemeHow it worksLatencyStarvation risk
Fixed priorityMaster 0 always wins over Master 1 if both request simultaneously0 cycles (combinational)High — low-priority master may never get bus
Round-robinAfter granting to Master N, next grant goes to Master N+1 (wraps around)1 cycle (register-based)None — fair under sustained load
Weighted round-robinEach master gets a burst of N grants before rotating1 cycleLow — configurable minimum bandwidth

The key insight: the arbiter's grant signal is a single bit. A single-bit signal crossing clock domains can be safely handled with a 2-FF synchronizer (Days 2–3). This is why arbitration is a powerful CDC strategy — it converts a wide multi-bit bus ownership problem into a narrow 1-bit control crossing.

Bus Arbiter: Two Domains Requesting a Shared Resource Domain A (CLK_A) Master / CPU REQ_A Domain B (CLK_B) Master / DMA REQ_B ARBITER Round-Robin or Priority 2-FF sync 2-FF sync GNT_A GNT_B Shared Resource
Figure — Two clock domains assert REQ; arbiter grants to exactly one; grant is synchronized into each domain via 2-FF synchronizer before data transfer begins.

3. Mutex Arbiter for CDC — Metastability-Resistant Mutual Exclusion

A standard round-robin arbiter is a registered circuit — it samples REQ signals on its own clock. But when the arbiter itself operates asynchronously (no dedicated arbiter clock), or when two REQs arrive within picoseconds of each other, even the arbiter can enter a metastable state.

The solution is the MUTEX cell — a mutual exclusion element that guarantees metastability-free arbitration even when both requests arrive simultaneously. The MUTEX is built from two cross-coupled NAND gates (an SR latch variant) with output enables that guarantee exactly one output can be asserted at a time.

MUTEX guarantee

A MUTEX cell guarantees: (1) at most one output is ever HIGH; (2) if one request is pending when the other arrives, the earlier one wins immediately; (3) if both arrive simultaneously, the cell resolves to exactly one — waiting as long as necessary. Resolution time is unbounded but probabilistically short (nanoseconds in practice).

Why standard cells are insufficient

A standard SR latch from your cell library cannot serve as a MUTEX because the library cell is characterized and optimized for normal operation — its cross-coupled transistors may be perfectly balanced, creating a symmetric metastable state that lingers. A proper MUTEX requires:

In most SoC designs, the MUTEX cell is provided by the standard cell library vendor with a special characterization file. You instantiate it directly — you do not implement it from gates.

4. MUTEX Cell Implementation

Conceptually, a MUTEX cell looks like this in Verilog — though in practice you always use the library-provided cell, not this RTL:

mutex_cell_concept.v
// CONCEPTUAL ONLY -- do NOT synthesize this.
// Real MUTEX cells come from IP library with custom layout.
// This illustrates the principle only.

module mutex_cell (
  input  wire req_a,    // Request from domain A
  input  wire req_b,    // Request from domain B
  output wire gnt_a,    // Grant to domain A (never simultaneous with gnt_b)
  output wire gnt_b     // Grant to domain B (never simultaneous with gnt_a)
);
  // Cross-coupled NAND gates -- SR latch structure.
  // In real implementation: custom transistor sizing ensures asymmetric
  // resolution so one side always wins before the other.
  wire q_a, q_b;

  // NAND-based SR latch (conceptual)
  assign q_a = ~(req_a & q_b);
  assign q_b = ~(req_b & q_a);

  // Output enables: only assert grant when latch is fully settled.
  // In real cell, this is a metastability filter (resolution detection) circuit.
  assign gnt_a = q_a & ~q_b;   // settled: A won
  assign gnt_b = q_b & ~q_a;   // settled: B won

  // KEY PROPERTY: gnt_a & gnt_b == 0 always (mutual exclusion)
  // KEY PROPERTY: gnt_a | gnt_b == 1 eventually   (liveness)
endmodule

5. Arbiter-Based CDC Pattern — Full Protocol

The complete arbiter-based CDC protocol follows this sequence. Note that control must be confirmed stable before data is driven:

  1. Request phase: Master_A asserts REQ_A to the arbiter; master places data stable on the bus simultaneously
  2. Grant phase: Arbiter asserts GNT_A (single bit); GNT_A is synchronized into Domain A via 2-FF sync
  3. Confirmation phase: Domain A waits for synchronized grant to be HIGH for 2+ cycles (confirms it is not a glitch)
  4. Transfer phase: Domain A drives address + data onto shared bus (already stable from step 1)
  5. Release phase: After transfer completes, Domain A de-asserts REQ_A; arbiter withdraws grant; next master gets bus
Arbiter-Based CDC Timing — Control Before Data REQ_A GNT_A GNT_sync DATA_BUS REQ asserted arbiter grants 2-FF latency data valid
Figure — REQ_A is asserted first; arbiter grants; 2-FF synchronizer adds latency; data is only driven onto the shared bus after the synchronized grant is confirmed stable in the destination domain.

6. Token-Passing CDC

For systems with exactly two clock domains sharing one resource, a token-passing scheme is simpler and often more efficient than a full arbiter. Only the token holder is allowed to initiate a transfer. The token is a single-bit register that crosses from one domain to the other using a proper toggle synchronizer handshake.

The protocol works as follows:

token_cdc.v
// Token-passing CDC for 2-domain shared resource.
// Domain A holds the token at reset; passes it when done.

module token_cdc (
  input  wire clk_a, rst_a,
  input  wire clk_b, rst_b,

  // Domain A
  input  wire req_a,        // A wants to use the resource
  output reg  gnt_a,        // A currently holds the token
  input  wire done_a,       // A signals transfer complete

  // Domain B
  input  wire req_b,
  output reg  gnt_b,
  input  wire done_b
);
  // Toggle registers: toggled to signal ownership transfer
  reg toggle_ab;    // A toggles this to pass token to B
  reg toggle_ba;    // B toggles this to pass token back to A

  // 2-FF synchronizers
  reg [1:0] sync_ab_b;   // toggle_ab synced into clk_b
  reg [1:0] sync_ba_a;   // toggle_ba synced into clk_a

  always @(posedge clk_b or posedge rst_b) begin
    if (rst_b) sync_ab_b <= 2'b00;
    else        sync_ab_b <= {sync_ab_b[0], toggle_ab};
  end

  always @(posedge clk_a or posedge rst_a) begin
    if (rst_a) sync_ba_a <= 2'b00;
    else        sync_ba_a <= {sync_ba_a[0], toggle_ba};
  end

  // Edge detectors: level change on toggle = ownership transferred
  reg last_ab_b, last_ba_a;
  wire got_token_b = sync_ab_b[1] ^ last_ab_b;
  wire got_token_a = sync_ba_a[1] ^ last_ba_a;

  // Domain A: holds token at reset
  always @(posedge clk_a or posedge rst_a) begin
    if (rst_a) begin
      gnt_a     <= 1'b1;
      toggle_ab <= 1'b0;
      last_ba_a <= 1'b0;
    end else begin
      last_ba_a <= sync_ba_a[1];
      if (got_token_a) gnt_a <= 1'b1;     // token returned from B
      if (gnt_a && done_a) begin
        gnt_a     <= 1'b0;
        toggle_ab <= ~toggle_ab;           // pass token to B
      end
    end
  end

  // Domain B: waits for token
  always @(posedge clk_b or posedge rst_b) begin
    if (rst_b) begin
      gnt_b     <= 1'b0;
      toggle_ba <= 1'b0;
      last_ab_b <= 1'b0;
    end else begin
      last_ab_b <= sync_ab_b[1];
      if (got_token_b) gnt_b <= 1'b1;     // received token from A
      if (gnt_b && done_b) begin
        gnt_b     <= 1'b0;
        toggle_ba <= ~toggle_ba;           // pass token back to A
      end
    end
  end
endmodule

7. Control Path vs Data Path Timing

The single most common mistake in arbiter-based CDC is violating the control-before-data rule: the control signal (grant, enable, valid) must be seen by the destination domain before it samples data. The timing constraint has three components:

PhaseRequirementMechanism
Data setupData must be stable in source domain BEFORE control is assertedRTL ordering: drive data first, assert control only after data is stable
Control crossingControl signal synchronized through 2-FF or 3-FF sync2-FF synchronizer, adds 2–3 cycle latency in destination domain
Data holdData must remain stable for 2+ cycles after control de-assertionRTL protocol: source holds data until a synchronized ACK is received back

The data itself does not need to be synchronized — it is driven by the source domain and held stable until the destination domain confirms (via a synchronized ACK handshake) that it has captured the data. This is why you synchronize narrow control signals and use the wide data bus freely — as long as data is stable when control is captured.

The golden rule of CDC data transfer

In CDC, data follows control. Control tells the receiver "data is ready." Control crosses through the synchronizer. Data is stable before control rises, and stays stable until an ACK confirms capture. Never assert control before data is stable on the source side.

8. Semaphore-Based Resource Sharing

When shared memory (dual-port RAM, shared register file) is accessed by two clock domains, a semaphore handshake prevents simultaneous read/write corruption. A semaphore is a single-bit flag indicating resource ownership, transferred with a 4-phase handshake: REQ → ACK → REL → ACK_REL.

The key properties of a CDC semaphore:

semaphore_cdc.v
// CDC Semaphore: shared memory access from two clock domains.
// Uses 4-phase handshake: REQ -> ACK -> REL -> ACK_REL.

module cdc_semaphore (
  input  wire clk_a, rst_a,
  input  wire clk_b, rst_b,

  // Domain A interface
  input  wire req_a,        // A requests ownership
  output wire ack_a,        // A is granted ownership
  input  wire rel_a,        // A releases ownership

  // Domain B interface
  input  wire req_b,
  output wire ack_b,
  input  wire rel_b
);
  // Ownership register: 1 = domain A owns, 0 = domain B owns.
  // Lives in clk_a domain; B's view is synchronized.
  reg own_a;

  // 2-FF synchronizers
  reg [1:0] own_a_sync;   // own_a synced into clk_a (reading in own domain, direct)
  reg [1:0] own_b_sync;   // ~own_a synced into clk_b

  always @(posedge clk_a or posedge rst_a)
    if (rst_a) own_a_sync <= 2'b11;       // A owns at reset
    else        own_a_sync <= {own_a_sync[0], own_a};

  always @(posedge clk_b or posedge rst_b)
    if (rst_b) own_b_sync <= 2'b00;       // B does not own at reset
    else        own_b_sync <= {own_b_sync[0], ~own_a};

  assign ack_a = own_a_sync[1];            // A sees it owns resource
  assign ack_b = own_b_sync[1];            // B sees it owns resource

  // A releases: clear own_a in clk_a domain
  always @(posedge clk_a or posedge rst_a) begin
    if (rst_a)      own_a <= 1'b1;
    else if (rel_a) own_a <= 1'b0;       // A releases, B gets it
  end

  // B releases: must use a pulse synchronizer to set own_a in clk_a.
  // Shown here as a toggle-based pulse synchronizer:
  reg        toggle_b_rel;
  reg [1:0]  sync_rel_a;
  reg        last_rel_a;
  wire       rel_b_pulse_a;

  always @(posedge clk_b or posedge rst_b)
    if (rst_b) toggle_b_rel <= 1'b0;
    else if (rel_b) toggle_b_rel <= ~toggle_b_rel;

  always @(posedge clk_a or posedge rst_a)
    if (rst_a) begin sync_rel_a <= 2'b00; last_rel_a <= 1'b0; end
    else begin
      sync_rel_a <= {sync_rel_a[0], toggle_b_rel};
      last_rel_a <= sync_rel_a[1];
    end

  assign rel_b_pulse_a = sync_rel_a[1] ^ last_rel_a;

  always @(posedge clk_a or posedge rst_a)
    if (rst_a) ;
    else if (rel_b_pulse_a) own_a <= 1'b1;   // B released, A gets ownership back
endmodule

9. Complete Verilog: Dual-Port RAM with Arbitrated Access from Two Domains

This is the practical implementation you will encounter in real SoC designs — a dual-port RAM where Port A is clocked by clk_a and Port B by clk_b, with a round-robin arbiter preventing simultaneous writes to the same address:

dual_port_ram_arbiter.v
// Dual-port RAM with CDC arbiter for two clock domains.
// Reads are unrestricted (both ports read freely).
// Writes are arbitrated: only one port writes at a time.
// Arbiter runs in clk_a; clk_b write requests are synchronized in.

module dp_ram_cdc_arb #(
  parameter DEPTH = 256,
  parameter WIDTH = 32,
  parameter ABITS = 8
)(
  // Port A
  input  wire           clk_a, rst_a,
  input  wire           wr_req_a,
  input  wire [ABITS-1:0] addr_a,
  input  wire [WIDTH-1:0] wdata_a,
  output reg            wr_ack_a,

  // Port B
  input  wire           clk_b, rst_b,
  input  wire           wr_req_b,
  input  wire [ABITS-1:0] addr_b,    // held stable until wr_ack_b
  input  wire [WIDTH-1:0] wdata_b,   // held stable until wr_ack_b
  output reg            wr_ack_b,

  // Read ports (combinational, no arbitration needed)
  input  wire [ABITS-1:0] rd_addr_a,
  output wire [WIDTH-1:0] rd_data_a,
  input  wire [ABITS-1:0] rd_addr_b,
  output wire [WIDTH-1:0] rd_data_b
);
  // ---- Memory array ----
  reg [WIDTH-1:0] mem [0:DEPTH-1];

  // ---- Sync wr_req_b into clk_a via toggle synchronizer ----
  reg        toggle_b;
  reg [1:0]  sync_tog_a;
  reg        last_tog_a;

  always @(posedge clk_b or posedge rst_b)
    if (rst_b) toggle_b <= 1'b0;
    else if (wr_req_b && !wr_ack_b) toggle_b <= ~toggle_b;

  always @(posedge clk_a or posedge rst_a)
    if (rst_a) begin sync_tog_a <= 2'b00; last_tog_a <= 1'b0; end
    else begin
      sync_tog_a <= {sync_tog_a[0], toggle_b};
      last_tog_a <= sync_tog_a[1];
    end

  wire wr_req_b_in_a = sync_tog_a[1] ^ last_tog_a;  // pulse in clk_a

  // ---- Round-robin arbiter in clk_a ----
  reg last_grant;   // 0 = last grant was to A, 1 = to B
  reg [1:0] state;
  reg wr_en_a, wr_en_b;
  reg [ABITS-1:0] arb_addr;
  reg [WIDTH-1:0] arb_data;

  localparam IDLE    = 2'd0,
             GRANT_A = 2'd1,
             GRANT_B = 2'd2;

  always @(posedge clk_a or posedge rst_a) begin
    if (rst_a) begin
      state <= IDLE; last_grant <= 1'b0;
      wr_ack_a <= 1'b0; wr_en_a <= 1'b0; wr_en_b <= 1'b0;
    end else begin
      wr_ack_a <= 1'b0; wr_en_a <= 1'b0; wr_en_b <= 1'b0;

      case (state)
        IDLE: begin
          if (wr_req_a && wr_req_b_in_a) begin
            if (!last_grant) begin
              state <= GRANT_A; last_grant <= 1'b0;
            end else begin
              state <= GRANT_B; last_grant <= 1'b1;
            end
          end else if (wr_req_a) begin
            state <= GRANT_A;
          end else if (wr_req_b_in_a) begin
            state <= GRANT_B;
          end
        end

        GRANT_A: begin
          arb_addr <= addr_a;
          arb_data <= wdata_a;
          wr_en_a  <= 1'b1;
          wr_ack_a <= 1'b1;
          state    <= IDLE;
        end

        GRANT_B: begin
          // B holds addr_b/wdata_b stable until it receives wr_ack_b.
          // Reading them here in clk_a is safe: B guarantees stability.
          arb_addr <= addr_b;
          arb_data <= wdata_b;
          wr_en_b  <= 1'b1;
          state    <= IDLE;
          // wr_ack_b is issued via toggle sync back to clk_b (below)
        end

        default: state <= IDLE;
      endcase
    end
  end

  // ---- Sync wr_ack back to clk_b ----
  reg        toggle_ack_a;
  reg [1:0]  sync_ack_b;
  reg        last_ack_b;

  always @(posedge clk_a or posedge rst_a)
    if (rst_a) toggle_ack_a <= 1'b0;
    else if (wr_en_b) toggle_ack_a <= ~toggle_ack_a;

  always @(posedge clk_b or posedge rst_b)
    if (rst_b) begin sync_ack_b <= 2'b00; last_ack_b <= 1'b0; end
    else begin
      sync_ack_b <= {sync_ack_b[0], toggle_ack_a};
      last_ack_b <= sync_ack_b[1];
    end

  always @(posedge clk_b or posedge rst_b)
    if (rst_b) wr_ack_b <= 1'b0;
    else        wr_ack_b <= sync_ack_b[1] ^ last_ack_b;   // one-cycle pulse

  // ---- RAM write (single write port, arbitrated) ----
  always @(posedge clk_a)
    if (wr_en_a || wr_en_b) mem[arb_addr] <= arb_data;

  // ---- RAM reads (asynchronous) ----
  assign rd_data_a = mem[rd_addr_a];
  assign rd_data_b = mem[rd_addr_b];

endmodule

10. Real Example: AHB Bus with Two Masters in Different Domains

In ARM's AMBA AHB bus, the bus matrix handles multiple masters. When Master 0 is in the CPU clock domain (1.2 GHz) and Master 1 is a DMA controller in a 200 MHz low-power domain, the AHB arbiter must cross grant signals. The standard approach used in ARM Cortex-M and Cortex-A SoCs:

SignalDirectionCDC methodNotes
HBUSREQDMA (clk_b) → Arbiter (clk_a)2-FF synchronizerSingle-bit request; safe to synchronize directly
HGRANTArbiter (clk_a) → DMA (clk_b)2-FF synchronizerSingle-bit grant; 2–3 cycle latency is acceptable
HADDR[31:0]DMA → Shared busNo synchronizerDMA holds address stable until HGRANT is seen; data is static during grant window
HWDATA[31:0]DMA → Shared busNo synchronizerDMA holds write data stable until HREADY is returned from slave
HRDATA[31:0]Slave → DMA (clk_b)Register in clk_bHREADY is synchronized first; only then is HRDATA captured into clk_b register
HREADYSlave → DMA (clk_b)2-FF synchronizerSingle-bit ready; synchronized before HRDATA capture

The key pattern: only single-bit control signals are synchronized. Wide data buses are kept stable by protocol — the source domain holds data until the destination acknowledges via a synchronized control pulse. This minimizes synchronizer hardware and maximizes throughput.

Industry practice: ARM's bus matrix RTL (used in Cortex-M and Cortex-A SoCs) implements exactly this pattern. CDC sign-off tools (Synopsys SpyGlass CDC, Cadence JasperGold CDC) verify that no unsynchronized multi-bit crossing exists and that data is held stable during the required window.

11. Choosing the Right Scheme

ScenarioRecommended schemeWhy
2 domains, infrequent transfersToken-passingSimplest; no arbiter logic; starvation-free by construction
2 domains, burst transfersHandshake + semaphoreKeeps bus for multiple beats before releasing ownership
3+ domains, round-robin fairnessRegistered round-robin arbiter + grant synchronizersProven in AHB/AXI bus matrices; scales to N masters
Async request timing unknownMUTEX cell + arbiterMetastability-resistant when request timing is truly asynchronous
High-performance NoCCredit-based flow controlAvoids blocking; combines CDC with flow control; used in AXI4

Day 8 Takeaways

FAQ

Why can't you synchronize control and data signals independently in CDC?

When control (enable/valid) and data signals are synchronized independently through separate 2-FF synchronizers, they arrive at the destination domain at different times due to different synchronizer latencies and routing delays. The receiver may see a control pulse that doesn't correspond to stable data, causing an incorrect capture. You must ensure data is stable before control is asserted, and hold data stable until the destination acknowledges capture via a synchronized ACK.

What is a MUTEX cell and why is it needed for CDC arbitration?

A MUTEX (mutual exclusion) cell is a special circuit built from cross-coupled NAND gates that guarantees only one output is asserted even when both inputs go high simultaneously. Standard cells cannot fulfill this role because perfectly balanced transistors may enter a symmetric metastable state that never resolves. A proper MUTEX uses custom layout with deliberate asymmetry to ensure eventual resolution to exactly one grant, making it safe for arbitration across asynchronous clock domains.

What is the control-before-data rule in arbiter-based CDC?

In arbiter-based CDC, the grant signal (control) must cross to the receiving domain and be fully stable before data is allowed to transfer. The sequence is: source asserts REQ and holds data stable; arbiter asserts GRANT; GRANT is synchronized through 2 FFs (2-cycle latency); destination sees stable GRANT; only then does it sample data. If data is driven before the synchronized GRANT is seen, the receiver may sample invalid values.

Previous
← Day 7: Reset Synchronization

← Full course roadmap