Skip to content
VLSI Mentor

DDR · Module 6

DQ — The Data Bus

Saying DQ is bidirectional says almost nothing. What matters is who drives these wires right now, how ownership changes, and what guarantees both sides never drive at once — which is a state machine, not a property.

Every signal so far in this module has had fixed ownership. The controller side drives CK, CKE, CS#, the command pins, ODT and RESET#, and no DRAM device ever drives any of them. Direction was never a question.

DQ is where that ends, and the usual way of saying so is nearly useless:

DQ is bidirectional.

That is true and it answers nothing. It does not say who is driving now, how the change happens, what prevents both sides driving at once, or what a controller must know in advance. "Bidirectional" describes a capability; what an engineer needs is a contract.

So this chapter's question is:

Who drives these wires right now, and what guarantees the answer is never "both"?

The answer is a state machine, and building it is where this module's RTL gets substantial.

1. Ownership, Not Direction

Direction is a property of a transfer. Ownership is a property of a moment, and it is the one that matters.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   WRITE      controller / PHY drives DQ          DRAM receives
   READ       DRAM drives DQ                      controller / PHY receives
   NEITHER    nobody drives                       the bus is idle
   BOTH       contention -- no useful data at all

The fourth row is not an error condition to be handled; it is a condition to be made impossible. Two drivers fighting on a shared conductor do not produce a corrupted value that could be detected — they produce a voltage that is a function of both drivers' strengths, on every contended line, with nothing anywhere reporting it.

And the third row is not idleness in the sense of "nothing happening". Chapter 6.5 §4 established that a column command commits the bus to a transfer beginning a fixed interval later. A bus with nobody driving may be fully committed, and a controller that reasons about observed activity rather than committed obligations will collide.

2. Three Signals, Not One Wire

Now the modelling decision, which is the most transferable thing in this chapter.

A bidirectional wire is naturally written in SystemVerilog as an inout. It is also the wrong way to model one at the controller's abstraction level, and the reason is precise: an inout hides exactly the thing you need to reason about.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   inout  logic [W-1:0] dq;          // who is driving? unanswerable

   logic [W-1:0] dq_out;             // what this side would drive
   logic         dq_oe;              // whether it is driving  <-- ownership
   logic [W-1:0] dq_in;              // what this side receives
A single bidirectional wire hides which side is driving, so the question cannot be answered from the model and contention cannot be detected. Splitting it into an output value, an output enable and an input makes ownership an explicit signal, so the output enable answers the ownership question directly and simultaneous driving becomes detectable and assertable.One inout wireownership hiddenWho drives?unanswerableContention invisiblenothing to checkout + oe + inownership explicitdq_oe answers ita signal, not a guessContention checkableassertablegivesgivesinstead12
Figure 1 — splitting the wire makes ownership a signal you can reason about and check.

dq_oe is the whole point. It turns "who owns the bus" from an architectural question into a signal that exists, can be traced, can be asserted about, and can be found wrong in simulation. With an inout, the equivalent question has no representation.

What sits below this abstraction, and why it is not RTL's business. The actual combination of dq_out, dq_oe and dq_in into a physical bidirectional pad is done by an I/O buffer — a cell with a driver, a receiver and an enable, instantiated or inferred differently on every FPGA family and every ASIC process. How that cell is inferred, what its drive strength and termination are, and how its timing is characterised are all tool- and technology-specific, and they belong below the controller boundary. Modules 19 to 22 own the PHY.

So the split is not a simulation trick. It is the correct interface at this level, and the pad is a component the level below supplies.

3. An Interface as a Boundary

Because the split produces several related signals per family, a SystemVerilog interface is a natural way to express the boundary — and it makes the design-versus-verification distinction explicit.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// EDUCATIONAL INTERFACE -- a design and verification BOUNDARY, not a
// complete DDR interface.
//
// Present: the signal families this module has covered, in the split
// representation of Section 2.
//
// ABSENT, and deliberately: every analog property, the differential nature
// of CK and DQS, ODT's termination network, the multi-function pin
// interpretation of Chapter 6.6, and all timing. This describes WHAT
// SIGNALS EXIST and WHO DRIVES THEM. It is not a JEDEC interface and
// cannot be connected to a real device.
// ─────────────────────────────────────────────────────────────────────────
interface ddr_signal_if #(
  parameter int DQ_W  = 8,
  parameter int DQS_W = 1,
  parameter int CA_W  = 24,
  parameter int RANKS = 2
) ();

  // ── Controller-driven, fixed ownership (Chapters 6.1-6.8).
  logic              ck;          // differential in reality; one bit here
  logic              reset_n;
  logic [CA_W-1:0]   ca;
  logic [RANKS-1:0]  cs_n;
  logic [RANKS-1:0]  odt;

  // ── Bidirectional, split so ownership is explicit (this chapter).
  logic [DQ_W-1:0]   dq_out;
  logic [DQ_W-1:0]   dq_in;
  logic              dq_oe;

  // ── The strobe, same discipline (Chapter 6.10).
  logic [DQS_W-1:0]  dqs_out;
  logic [DQS_W-1:0]  dqs_in;
  logic              dqs_oe;

  // ── A DESIGN view drives what the controller drives and observes what
  //    it receives. Note dq_in is an INPUT here: a controller never drives
  //    it, and declaring it so makes that structural rather than trusted.
  modport ctrl (
    output ck, reset_n, ca, cs_n, odt,
    output dq_out, dq_oe, dqs_out, dqs_oe,
    input  dq_in, dqs_in
  );

  // ── A VERIFICATION view observes everything and drives nothing. A
  //    monitor that can drive is a monitor that can perturb what it
  //    measures, and the modport makes that impossible rather than
  //    forbidden by convention.
  modport mon (
    input ck, reset_n, ca, cs_n, odt,
    input dq_out, dq_oe, dq_in, dqs_out, dqs_oe, dqs_in
  );

endinterface

The two modports are the reason this is worth showing. A design view drives the controller's outputs and can only read its inputs; a monitor view reads everything and drives nothing. Those constraints are enforced by the language rather than by discipline — a monitor that tried to drive would not compile.

And notice what the monitor sees that a real monitor cannot. dq_oe is a controller-internal signal; on a physical interface, a probe sees only the wire. Chapter 6.4 §9's lesson applies again: a bus monitor sees values, and inferring ownership from values is the hard part. §9 is about exactly that.

4. RTL — The Ownership State Machine

Engineering problem

Own the data bus correctly. Drive it during writes, release it before a read's data arrives, capture only while read ownership is valid, never drive during a read, and separate consecutive transfers by a configurable turnaround gap.

Classification

SYNTHESIZABLE RTL — an educational controller/PHY-boundary abstraction.

What it represents: temporal ownership of a bidirectional bus, expressed as states with explicit output-enable control, plus contention detection.

What it does not represent, and the list matters: the state names below are not JEDEC command states — they are an educational decomposition of the ownership problem, and no specification defines them. TURNAROUND_GAP is a configurable pedagogical gap; it is not a bus-turnaround timing parameter, and this chapter deliberately does not name any. READ_LATENCY is the same kind of stand-in Chapter 6.5 §4 used. Modules 13 and 14 own the real parameters.

Also absent: the I/O buffer, all electrical behaviour, DQS itself — only its enable is referenced, and Chapter 6.10 owns the signal — and any data content or correctness.

Interface contract

write_req and read_req with req_beats request a transfer. dq_out, dq_oe, dq_in are the split bus. capture_en qualifies received data. dram_driving is an input modelling the far side, present so contention is detectable. contention and illegal_req report the two failure classes.

State

The ownership state, a beat counter, a latency countdown and a turnaround countdown.

Combinational behaviour

dq_oe derived from state — one source of truth for ownership — and contention detection.

Sequential behaviour

The state machine and its counters.

How to simulate

vlog dq_bus_ownership.sv tb_dq_bus_ownership.sv then vsim -c tb_dq_bus_ownership -do "run -all".

Expected result: dq_oe asserts only in the write-drive state, never during read states; a turnaround gap separates transfers; capture_en asserts only during read capture; and driving dram_driving high while dq_oe is asserted reports contention.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// DQ BUS OWNERSHIP.
// Classification: SYNTHESIZABLE RTL -- an educational CONTROLLER/PHY
// BOUNDARY abstraction.
//
// "DQ is bidirectional" says nothing useful. What matters is TEMPORAL
// OWNERSHIP: who drives right now, how it changes, and what guarantees
// the answer is never "both". That is a state machine.
//
// THE STATE NAMES BELOW ARE NOT JEDEC COMMAND STATES. They are an
// educational decomposition of the ownership problem and no specification
// defines them.
//
// TURNAROUND_GAP IS A CONFIGURABLE PEDAGOGICAL GAP. It is NOT a bus
// turnaround timing parameter, and this chapter names none. READ_LATENCY
// is the same kind of stand-in Chapter 6.5 used. Modules 13 and 14 own the
// real parameters. DO NOT SIZE ANYTHING FROM THESE.
//
// ALSO ABSENT: the I/O buffer, all electrical behaviour, DQS itself (only
// its enable is referenced -- Chapter 6.10 owns the signal), and any data
// content or correctness.
//
// OWNERSHIP IS DERIVED FROM STATE, never held separately, so there is
// exactly one source of truth about who is driving.
// ─────────────────────────────────────────────────────────────────────────
module dq_bus_ownership #(
  parameter int DQ_W           = 8,
  parameter int MAX_BEATS      = 8,
  // Events from a read request to its data window. EDUCATIONAL.
  parameter int READ_LATENCY   = 2,
  // Events the bus is left undriven between transfers. EDUCATIONAL --
  // a pedagogical gap, not tWTR, tRTW or any named parameter.
  parameter int TURNAROUND_GAP = 2,
  parameter int BEAT_W = (MAX_BEATS      <= 1) ? 1 : $clog2(MAX_BEATS),
  parameter int LAT_W  = (READ_LATENCY   <= 1) ? 1 : $clog2(READ_LATENCY + 1),
  parameter int TA_W   = (TURNAROUND_GAP <= 1) ? 1 : $clog2(TURNAROUND_GAP + 1)
) (
  input  logic              clk,
  input  logic              rst_n,

  input  logic              write_req,
  input  logic              read_req,
  input  logic [BEAT_W:0]   req_beats,
  input  logic [DQ_W-1:0]   write_data,

  // ── The split bus of Section 2.
  output logic [DQ_W-1:0]   dq_out,
  output logic              dq_oe,
  input  logic [DQ_W-1:0]   dq_in,

  // The strobe's enable travels with ownership. The SIGNAL is Chapter
  // 6.10's; only the ownership relationship belongs here.
  output logic              dqs_oe,

  output logic              capture_en,
  output logic [DQ_W-1:0]   captured_data,
  output logic [BEAT_W-1:0] beat_index,

  // ── Models the far side driving. Present so contention is DETECTABLE:
  //    a hazard that cannot be represented cannot be checked, and the
  //    whole point of Section 2's split is to make this observable.
  input  logic              dram_driving,
  output logic              contention,

  output logic              illegal_req,
  output logic [2:0]        state_out
) ;

  // ── COMPILE-TIME legality.
  if (DQ_W < 1) begin : g_dq
    initial $fatal(1, "dq_bus_ownership: DQ_W must be >= 1");
  end
  if (MAX_BEATS < 1) begin : g_mb
    initial $fatal(1, "dq_bus_ownership: MAX_BEATS must be >= 1");
  end
  if (READ_LATENCY < 1) begin : g_rl
    initial $fatal(1, "dq_bus_ownership: READ_LATENCY must be >= 1");
  end
  // A zero turnaround gap would let a write's drive and a read's capture
  // abut with no undriven interval -- which on real hardware is exactly
  // the contention this module exists to prevent. Rejected rather than
  // permitted, because the electrical need for a gap is not negotiable
  // even though its DURATION is out of scope here.
  if (TURNAROUND_GAP < 1) begin : g_ta
    initial $fatal(1, "dq_bus_ownership: TURNAROUND_GAP must be >= 1");
  end

  typedef enum logic [2:0] {
    S_IDLE          = 3'd0,
    S_WRITE_DRIVE   = 3'd1,
    S_WRITE_RELEASE = 3'd2,
    S_READ_WAIT     = 3'd3,
    S_READ_CAPTURE  = 3'd4
  } own_e;

  own_e              st_q;
  logic [BEAT_W:0]   beats_q;
  logic [BEAT_W-1:0] index_q;
  logic [LAT_W-1:0]  lat_q;
  logic [TA_W-1:0]   ta_q;
  logic [DQ_W-1:0]   wdata_q;

  assign state_out  = st_q;
  assign beat_index = index_q;

  // ── OWNERSHIP, DERIVED. dq_oe is a function of state alone. There is no
  //    path by which it can be asserted in a read state, which makes the
  //    safety property of Section 5 true BY CONSTRUCTION rather than by
  //    maintenance -- the strongest form available.
  assign dq_oe  = (st_q == S_WRITE_DRIVE);
  assign dqs_oe = dq_oe;
  assign dq_out = wdata_q;

  assign capture_en = (st_q == S_READ_CAPTURE);

  // ── Contention. Both sides driving at once. Not recoverable and not
  //    correctable -- two drivers on a conductor produce a voltage that is
  //    a function of both, on every contended line, with nothing
  //    reporting it. Detected here so it is visible in simulation.
  assign contention = dq_oe && dram_driving;

  logic idle_now, beats_ok;
  assign idle_now = (st_q == S_IDLE);
  assign beats_ok = (req_beats != '0) && (req_beats <= (BEAT_W+1)'(MAX_BEATS));

  // A request while a transfer is in progress, or with an illegal beat
  // count, or both directions at once. Reported, never queued -- queueing
  // is the scheduler's job (Module 17).
  assign illegal_req = (write_req || read_req)
                    && (!idle_now || !beats_ok || (write_req && read_req));

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st_q          <= S_IDLE;
      beats_q       <= '0;
      index_q       <= '0;
      lat_q         <= '0;
      ta_q          <= '0;
      wdata_q       <= '0;
      captured_data <= '0;
    end else begin
      if (capture_en) captured_data <= dq_in;

      unique case (st_q)

        S_IDLE: begin
          if (write_req && !read_req && beats_ok) begin
            st_q    <= S_WRITE_DRIVE;
            beats_q <= req_beats;
            index_q <= '0;
            wdata_q <= write_data;
          end else if (read_req && !write_req && beats_ok) begin
            st_q    <= S_READ_WAIT;
            beats_q <= req_beats;
            index_q <= '0;
            lat_q   <= LAT_W'(READ_LATENCY);
          end
        end

        S_WRITE_DRIVE: begin
          wdata_q <= write_data;
          if (beats_q <= (BEAT_W+1)'(1)) begin
            // Last beat driven. Release ownership and hold the bus
            // undriven for the turnaround gap before anything else may
            // take it.
            st_q <= S_WRITE_RELEASE;
            ta_q <= TA_W'(TURNAROUND_GAP);
          end else begin
            beats_q <= beats_q - (BEAT_W+1)'(1);
            index_q <= index_q + BEAT_W'(1);
          end
        end

        S_WRITE_RELEASE: begin
          // dq_oe is already low here -- it is a function of state. This
          // interval exists so the bus is DEMONSTRABLY undriven before the
          // far side may drive it.
          if (ta_q <= TA_W'(1)) st_q <= S_IDLE;
          else                  ta_q <= ta_q - TA_W'(1);
        end

        S_READ_WAIT: begin
          // The controller is NOT driving and the data has not arrived.
          // Chapter 6.5's committed-but-not-busy interval, now with an
          // ownership meaning: the bus belongs to the far side already.
          if (lat_q <= LAT_W'(1)) begin
            st_q    <= S_READ_CAPTURE;
            index_q <= '0;
          end else begin
            lat_q <= lat_q - LAT_W'(1);
          end
        end

        S_READ_CAPTURE: begin
          if (beats_q <= (BEAT_W+1)'(1)) begin
            st_q <= S_IDLE;
          end else begin
            beats_q <= beats_q - (BEAT_W+1)'(1);
            index_q <= index_q + BEAT_W'(1);
          end
        end

        default: st_q <= S_IDLE;
      endcase
    end
  end

endmodule

Cycle-by-cycle example

MAX_BEATS = 8, READ_LATENCY = 2, TURNAROUND_GAP = 2, two-beat transfers:

CycleStatedq_oecapture_enNote
0IDLE00write request arrives
1WRITE_DRIVE10driving beat 0
2WRITE_DRIVE10driving beat 1
3WRITE_RELEASE00released
4WRITE_RELEASE00gap
5IDLE00read request arrives
6READ_WAIT00not driving, no data yet
7READ_WAIT00
8READ_CAPTURE01capturing beat 0
9READ_CAPTURE01capturing beat 1

dq_oe is high on exactly two cycles out of ten, and is low in every read state. That column is the safety property, and because dq_oe is derived from state there is no reachable configuration in which it is high during a read.

Cycles 3 and 4 are the turnaround gap, and the important thing is what they are for: the bus is demonstrably undriven before the far side may drive it. Without such an interval, a write's last driven beat and a read's first driven beat would abut — which is the contention this whole structure exists to prevent, and is why TURNAROUND_GAP = 0 does not elaborate.

Cycles 6 and 7 are Chapter 6.5's committed-but-not-busy interval given an ownership meaning. The controller is not driving, no data has arrived, and the bus already belongs to the far side.

Waveform expectation

§6. Watch dq_oe and capture_en never assert together, and an undriven gap between every direction change.

Synthesis implication

A five-state machine, three counters and a data register. Small. dq_oe feeds the I/O buffer's enable, and its timing relative to the data is genuinely critical in physical implementation — releasing too late or driving too early is contention, and that timing is realised by the PHY, not by this logic. This block decides ownership; the PHY makes the transition electrically safe.

Corner cases

TURNAROUND_GAP == 0 does not elaborate, deliberately — the need for an undriven interval is not negotiable even though its duration is out of scope. MAX_BEATS == 1 gives single-beat transfers. A simultaneous write_req and read_req is reported as illegal and starts nothing, rather than arbitrarily picking one. A request during a transfer is refused and changes no state. Reset returns to IDLE with dq_oe low, which is the safe state — a reset that left the bus driven could contend with a device that had also been reset.

Debugging clues

If dq_oe is ever high during a read state, ownership is being held in a register rather than derived from state — the derivation is what makes the property structural. If contention appears at direction changes, check the turnaround gap and, more importantly, check whether the PHY's enable timing matches the logical state — a logically correct gap with a slow output-enable release still contends. If captured data is shifted, check that captured_data is registered on capture_en and that READ_LATENCY matches the far side. If transfers after the first are corrupt, check that index_q is reset on entry to each state rather than only at reset.

Limitations

No I/O buffer and no electrical behaviour — the actual pad, its drive strength, its termination and its enable timing are all below this abstraction and are tool- and technology-specific. No real timing: both cycle parameters are educational. No DQS signal, only its enable. No data correctness. No multiple outstanding transfers, which a real controller pipelines — this block handles one at a time and refuses overlap, the same conservative choice Chapter 6.5 §4 made and for the same reason.

5. Ownership Changing Hands

The controller drives the data bus during a write while the DRAM receives. The controller then releases the bus and an undriven turnaround interval follows, during which neither side drives. Only after that does the DRAM begin driving for a read while the controller receives and captures. The undriven interval is what prevents the two drivers overlapping.Write to read: ownership handoffController / PHYDRAM devicedq_oe asserted —driving write datafinal beat drivendq_oe released — busundriventurnaround gap —neither side drivesDRAM now drives readdatacontroller captures
Figure 2 — the handoff: drive, release, prove undriven, then the far side drives.

The fourth message is the one that matters and it is the one that looks like nothing. An interval in which neither side drives is what separates two drivers in time — and it exists because the alternative is not a corrupted transfer but a meaningless voltage on every line.

6. The Ownership Trace

dq_bus_ownership — write, release, turnaround, read capture

10 cycles
Ten cycles. A write request moves the state machine into the write drive state where the output enable is asserted for two beats. The machine then releases the bus and holds an undriven turnaround gap for two cycles. A read request moves it into a read wait state where it drives nothing and no data has arrived, then into read capture where the capture enable is asserted for two beats. The output enable is never asserted during any read state.write: dq_oe assertedwrite: dq_oeassertedundriven turnaroundundriven turnaroundread: capturingread: capturingcontroller owns the buscontroller owns the busreleased — undrivenreleased — undrivenfar side owns itfar side owns itCKstateIDLEWDRVWDRVWRELWRELIDLERWAITRWAITRCAPRCAPwrite_reqread_reqdq_oedq_out--D0D1--------------capture_endq_in----------------Q0Q1t0t1t2t3t4t5t6t7t8t9
Figure 3 — dq_oe high on two cycles, low in every read state, with a gap between.

dq_oe and capture_en never overlap, and the cycles between them are undriven by anybody. Read the trace as three ownership regions separated by a gap rather than as a sequence of transfers.

Cycles 6 and 7 are worth dwelling on. Nothing is on the bus and nothing is being driven, and this is not idle — a read is committed and its data is coming. Chapter 6.5 §5 showed the same interval from the scheduling side; here it has an owner.

Representative educational cycles. The two-cycle latency and two-cycle gap are chosen for legibility and correspond to no specification values.

7. Four Assertions Worth Writing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY, inside dq_bus_ownership.

// P1 -- THE SAFETY PROPERTY. The controller never drives during a read.
// Contention is not a correctable error: two drivers on a conductor
// produce a voltage that is a function of both, on every line, with
// nothing reporting it.
property p_no_drive_during_read;
  @(posedge clk) disable iff (!rst_n)
    ((st_q == S_READ_WAIT) || (st_q == S_READ_CAPTURE)) |-> !dq_oe;
endproperty
assert property (p_no_drive_during_read);

// P2 -- drive and capture are mutually exclusive. A weaker restatement of
// P1 on the outputs rather than on the states, so a state-encoding change
// cannot invalidate the check silently.
property p_drive_capture_exclusive;
  @(posedge clk) disable iff (!rst_n)
    !(dq_oe && capture_en);
endproperty
assert property (p_drive_capture_exclusive);

// P3 -- THE TURNAROUND PROPERTY. An undriven interval separates driving
// from capturing. Without it, a write's last driven beat and a read's
// first driven beat could abut -- which is the contention P1 forbids,
// arriving from the other side.
property p_gap_between_drive_and_capture;
  @(posedge clk) disable iff (!rst_n)
    $fell(dq_oe) |-> ##[1:TURNAROUND_GAP] !capture_en;
endproperty
assert property (p_gap_between_drive_and_capture);

// P4 -- capture only while read-owned. Capturing outside the window
// records whatever the bus happened to carry, which is plausible data
// from nowhere -- the worst kind, because nothing downstream objects.
property p_capture_only_when_owned;
  @(posedge clk) disable iff (!rst_n)
    capture_en |-> (st_q == S_READ_CAPTURE);
endproperty
assert property (p_capture_only_when_owned);

// P5 -- the liveness companion. P1 to P4 all forbid; a machine that never
// leaves IDLE satisfies every one of them.
property p_write_req_drives;
  @(posedge clk) disable iff (!rst_n)
    ((st_q == S_IDLE) && write_req && !read_req && beats_ok) |=> dq_oe;
endproperty
assert property (p_write_req_drives);

// P6 -- contention, if it is ever representable, is reported. The far side
// is an input here precisely so this can be checked; a hazard with no
// representation cannot be verified.
property p_contention_reported;
  @(posedge clk) disable iff (!rst_n)
    (dq_oe && dram_driving) |-> contention;
endproperty
assert property (p_contention_reported);

P1 is the property, and note that it is true by constructiondq_oe is derived from state, so there is no reachable assignment that violates it. That is deliberate and it is the right way to build a safety property: make the hazard unrepresentable, then assert it anyway in case the derivation is later changed.

P3 is the one most likely to be omitted. P1 forbids driving during a read, and a design could satisfy it perfectly while releasing the bus on the same cycle the far side begins driving. The gap is a separate requirement from the exclusion, and conflating them leaves a real hazard unchecked.

P6 exists because of a modelling decision. Contention is only assertable because dram_driving is an input — a hazard with no representation in the model cannot be verified at all, which is why §2's split matters beyond tidiness. An inout-based model has nothing to write P6 about.

What none of them prove — and this is the critical boundary. Nothing about the I/O buffer's enable timing, which is where real contention happens: a logically correct gap with a slow output-enable release still contends, and that is analog. Nothing about drive strength, termination or settling. Nothing about data correctness. A green regression here says ownership logic is correct and says nothing about whether the physical bus is ever contendedModules 19 to 22 own that, and it is verified by analog simulation and measurement.

8. Common Misconceptions

"DQ is bidirectional" is a sufficient description. Wrong model: direction capability is the relevant property. Why it is tempting: it is what datasheets say and it is true. Consequence: no model of when each side drives, so a controller has nothing to schedule against — and Chapter 6.5 §4 established that direction is committed a latency interval before it takes effect. A design reasoning about direction as a capability cannot know that the bus it sees as idle is already spoken for. Correct model: what matters is temporal ownership: who drives at each moment, with four possible states including "neither" and "both". "Neither" is often a committed bus, and "both" must be made impossible. Prevention: ask who is driving on a specific cycle. If the model cannot answer, it is not a model of the bus.

"DQ is driven by the memory controller." Wrong model: the controller owns the data bus. Why it is tempting: the controller initiates everything and owns every other signal in this module. Consequence: no release logic, so the controller drives while a read's data arrives — contention on every read. And a mental model in which capture is something the controller does whenever it likes, rather than only during a window it does not own. Correct model: the controller drives during writes only. During reads the DRAM drives and the controller receives. Ownership changes several times per transaction and the controller must plan each change in advance. Prevention: count the drivers. There are two, and exactly one may be active.

"Use an inout — that is what bidirectional means in SystemVerilog." Wrong model: the language construct for a bidirectional wire is the right abstraction at every level. Why it is tempting: it is literally the language feature for this, and it is correct at the pad. Consequence: ownership becomes unrepresentable. There is no signal meaning "I am driving", so contention cannot be detected, cannot be asserted about, and cannot be found in simulation. The most important property of the bus has no expression in the model. Correct model: at the controller level, use dq_out, dq_oe, dq_in. dq_oe is ownership. The combination into a physical pad is done by an I/O buffer whose inference and characterisation are tool- and technology-specific and belong below this boundary. Prevention: ask whether the model can answer "who is driving". An inout cannot.

"The controller can turn the bus around when it needs to." Wrong model: direction changes are reactive. Why it is tempting: it is how a simple request-response bus behaves, and it is how the problem feels from the outside. Consequence: a scheduler that cannot actually be built. Direction is fixed at command time and takes effect a latency interval later, so by the time the controller notices it wants the other direction, the commands governing the next several windows are already issued. A design assuming reactive turnaround will either contend or stall. Correct model: turnaround is scheduled. And the cost is not only electrical settling — it is the loss of scheduling freedom, which is why Chapter 4.5 §10 found mixed read/write traffic underperforming and why batching by direction is the fix. Prevention: ask when the decision was made. It was made when the command was issued, not when the data moves.

9. Debugging — Data Corruption Around Direction Changes

Symptom. A memory interface is reliable during long runs of reads and long runs of writes, and corrupts data when the traffic mixes. Corruption concentrates at the transitions.

Corruption localised at direction changes points at ownership, and the position of the corruption within a transfer identifies which obligation broke — the same discipline Chapter 5.4 §10 used for rank handoff.

Mechanism 1 — the controller drives into a read. Inspect: dq_oe (or the PHY's output enable) during read windows. Expected evidence: the enable asserted while read data is arriving — corruption on the first beats after a write-to-read change. Discriminator: is the enable ever asserted during a read window? This is §7's P1 violated, it is first because it is a single trace check, and on real hardware it is contention rather than a data error.

Mechanism 2 — the gap exists logically and not electrically. Inspect: the I/O buffer's enable release timing, not the logical state. Expected evidence: a logically correct turnaround gap with corruption still at the transition. Discriminator: does the logic look right while the hardware still fails? This is the boundary §7 named: a slow output-enable release contends even with a correct state machine, and no digital simulation shows it. Modules 19 to 22 own it.

Mechanism 3 — capture occurs outside the read window. Inspect: whether capture_en aligns with the arriving data. Expected evidence: plausible data captured from nowhere — values that are not garbage but are not the read's data either. Discriminator: is the data wrong or is it someone else's? Capturing outside the window records whatever the bus carried, which is the worst kind of corruption because nothing downstream objects.

Mechanism 4 — the read latency is misconfigured. Inspect: configured latency against the device and any module buffering. Expected evidence: a consistent shift rather than transition-localised corruption. Discriminator: is the fault at transitions or everywhere? Chapter 6.5 §9's mechanisms — a constant offset is a configuration number, not an ownership fault, and it is a different investigation entirely.

Mechanism 5 — ODT policy, not ownership. Inspect: termination during each direction. Expected evidence: marginality rather than corruption, worse at rate, with the accessed rank terminating during reads. Discriminator: is it corrupt or marginal? Chapter 6.7 §9's first mechanism, and it shares this symptom's correlation with direction while having a completely different cause.

Discrimination, cheapest first. Ask whether the corruption is at transitions or everywhere — one observation, splitting mechanism 4 off entirely. Then check whether the output enable is ever asserted during a read window. Then ask whether captured values are garbage or someone else's data. Then compare the logical gap against the PHY's actual enable timing.

The reasoning lesson. The distinction between "logically correct" and "electrically correct" is the boundary this whole chapter is built around, and it is exactly where this fault class lives. §2's split makes ownership checkable in RTL — and a design can pass every ownership assertion and still contend, because the enable's release timing is analog and outside every digital check. So a green regression on ownership logic is necessary and not sufficient, and knowing that is the difference between suspecting the state machine for a week and going straight to the PHY. Ask which layer the symptom's evidence actually implicates, not which layer you can most easily inspect.

10. Interview Reasoning

"Why is 'DQ is bidirectional' an inadequate description?" Because it describes a capability and what an engineer needs is a contract. It does not say who is driving at a given moment, how ownership changes, what prevents both sides driving at once, or what the controller must know in advance. The useful framing is temporal ownership with four states — controller driving, DRAM driving, neither, and both. "Neither" is important because a bus with nobody driving may already be committed to a transfer that has not started. "Both" is important because it is not a correctable error: two drivers on a conductor produce a voltage that is a function of both, on every line, with nothing reporting it.

"Why model a bidirectional bus as three signals instead of an inout?" Because an inout hides exactly the thing you need to reason about. With dq_out, dq_oe and dq_in, the output enable is ownership — it is a signal that exists, can be traced, can be asserted about and can be found wrong in simulation. With an inout there is no signal meaning "I am driving", so contention cannot be detected or checked at all; the most important property of the bus has no representation. The combination into a physical pad is done by an I/O buffer with a driver, receiver and enable, and how that cell is inferred and characterised is tool- and technology-specific, which is why it belongs below the controller abstraction rather than inside it.

"What is a bus turnaround, and why does it cost more than it looks?" It is the change of ownership, and the visible part is an interval during which neither side drives — which exists so the bus is demonstrably undriven before the far side takes it, rather than two drivers abutting. The cost that is easy to underestimate is not the settling interval but the loss of scheduling freedom. Direction is fixed when the column command is issued and takes effect a latency interval later, so the controller commits to a direction well before the bus points that way and cannot decide to turn around when it notices it needs to. That is why mixed read/write traffic underperforms and why batching by direction is the standard remedy.

"Can you verify that a DDR data bus is never contended?" Not in digital simulation, and understanding why is the important part. You can verify the ownership logic — that the output enable is never asserted during a read, that drive and capture are mutually exclusive, that an undriven gap separates them — and those are worth asserting. But real contention depends on the I/O buffer's enable release timing, which is analog: a logically correct gap with a slow enable release still contends, and no digital check sees it. So a green regression on ownership logic is necessary and not sufficient, and the remaining risk is verified by analog simulation and measurement at the PHY level.

"Data corrupts only when read and write traffic mixes. How do you narrow it down?" First by asking whether the corruption is localised at the transitions or present throughout, because a consistent shift everywhere is a latency misconfiguration — a constant offset is a configuration number and a completely different investigation. If it is localised at transitions, check whether the output enable is ever asserted during a read window, which is the direct contention case and a single trace lookup. Then ask whether the captured values are garbage or are someone else's plausible data, because capturing outside the read window records whatever the bus carried and nothing downstream objects to it. If the logic all looks correct and the hardware still fails at transitions, that is the boundary case: the gap exists logically and the I/O buffer's enable is releasing too slowly, which is analog and invisible to every digital check. And it is worth separating corruption from marginality, because a wrong ODT policy correlates with direction too and has an entirely different cause.

11. Engineering Exercise

Educational cycle counts; no timing parameters are implied.

1. A controller drives dq_oe high throughout a read. What happens on real hardware, and what would a scoreboard see? Contention — the controller and the DRAM both drive every line, producing a voltage that is a function of both drivers. A scoreboard would see wrong data, and would not be able to distinguish it from any other corruption. Nothing reports contention; that is why §7's P1 exists.

2. §4 rejects TURNAROUND_GAP = 0 at elaboration. Justify rejecting it rather than permitting it. With no gap, a write's last driven beat and the far side's first driven beat abut with no undriven interval — the contention the module exists to prevent. The duration is out of scope here and the existence is not negotiable, so permitting zero would let the model express a configuration that is always wrong.

3. §7's P1 forbids driving during a read. Construct a design that satisfies P1 and still contends. Release dq_oe on exactly the cycle the far side begins driving. P1 is satisfied — the enable is never high during a read state — and the two drivers still overlap at the boundary. That is why P3 is a separate property: exclusion and separation are different requirements.

4. Why is dram_driving an input to §4's model rather than being omitted? So that contention is representable. A hazard with no expression in the model cannot be asserted about, cannot be injected in a test, and cannot be found — §7's P6 has nothing to check without it. Making a hazard representable is a prerequisite for verifying its absence.

5. A controller decides mid-burst that it would prefer to read rather than write. Can it turn the bus around? No. Direction was fixed when the column command was issued, and the commands governing the next several data windows have already been issued. Turnaround is scheduled, not reactive — and a design assuming otherwise will either contend or stall.

6. §3's interface declares dq_in as an input on the ctrl modport. What class of bug does that prevent, and how? A controller driving the wire it should be receiving on. The modport makes it a compile error rather than a convention — the direction is enforced by the language. The mon modport does the same for monitors, which read everything and drive nothing, so a monitor that could perturb what it measures cannot be written.

12. Summary

"DQ is bidirectional" describes a capability; what matters is a contract. The useful framing is temporal ownership with four states: controller driving, DRAM driving, neither, and both. "Neither" is often a committed bus rather than an idle one. "Both" is not an error to handle but a condition to make impossible — two drivers produce a voltage that is a function of both, on every line, with nothing reporting it.

Model it as three signals, not one wire. dq_out, dq_oe, dq_in — and dq_oe is ownership, a signal that can be traced, asserted about and found wrong. An inout hides precisely that, leaving contention unrepresentable and therefore unverifiable. The physical pad is an I/O buffer supplied by the level below, whose inference and characterisation are tool- and technology-specific.

Ownership is a state machine, and the states are an educational decomposition — not JEDEC states. Drive during writes; release; hold an undriven gap; then the far side drives while the controller captures. dq_oe derived from state makes the safety property true by construction.

Exclusion and separation are different requirements. Never driving during a read is one property; an undriven interval between driving and capturing is another — and a design can satisfy the first and contend at the boundary.

Direction is committed a latency interval before it takes effect, so turnaround is scheduled, never reactive. The cost is not only electrical settling but the loss of scheduling freedom, which is why mixed read/write traffic underperforms and batching by direction is the remedy.

And the boundary is the chapter's real lesson. §2's split makes ownership checkable in RTL, and a design can pass every ownership assertion and still contend, because the I/O buffer's enable release timing is analog and outside every digital check. A green regression on ownership logic is necessary and not sufficient — and knowing which layer a symptom's evidence implicates is what separates a week on the state machine from going straight to the PHY.

13. What Comes Next

Chapter 6.10 takes the signal that travels with the data.

DQ carries values and needs something to say when they are valid. Chapter 6.1 §7 established that CK does not do this job and explained why: the skew between a separately distributed clock and the data it would time does not shrink as the transfer interval does.

DQS is the answer — and it is a genuine strobe, unlike RAS#. It is driven by whichever side drives the data, it travels with that data, and it has the same ownership discipline this chapter just built. Its enable appeared in §4's RTL for exactly that reason.

It is also the chapter where the temptation to write fake PHY RTL is strongest, and where the boundary this chapter drew has to be held most carefully.

Return to CAS# for the committed-window scheduling ownership depends on, WE# for where direction is decided, or Ranks for the rank-level ownership this chapter's bus is shared under. The full path is on the DDR tutorials index.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the DDR curriculum.