Skip to content
VLSI Mentor

USB · Module 9

Endpoint Addressing

One byte, four bits of number and one bit of direction — and the decoder that turns it into a slot. Why 0x81 and 0x01 are different hardware, and why an unimplemented endpoint must be refused rather than aliased.

Chapter 9.1 derived the requirement: a device needs several destinations, and the host must be able to name one. It did not say how.

The encoding is small — one byte, of which five bits matter — and its consequences are not:

An endpoint address is a pair, not a number. Four bits say which endpoint and one bit says which direction, and the pair selects one piece of hardware. Two addresses sharing a number are different endpoints with different buffers and different state.

A decoder that ignores one of those bits merges them, and the result works perfectly until both directions are used at once.

1. What Has To Be Encoded

Derive the field before reading it.

The host must name a destination inside a device, having already named the device itself with the address of Chapter 6.3. So the encoding answers: which of this device's endpoints?

And it must name a direction, because §2 shows the two directions of one endpoint number are separate resources with separate storage.

How much space does that need? A number, and one bit. Chapter 9.3 derives why the number is four bits; here it is enough that the field is small, because it rides in every transaction and every bit costs bus time.

2. The Encoding

The endpoint address is one byte, and Chapter 7.4 introduced it as bEndpointAddress:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bit    7     6     5     4     3     2     1     0
      ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
      │ DIR │  —  │  —  │  —  │       endpoint        │
      │     │     │     │     │        number         │
      └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
         │                     └──────────┬──────────┘
         │                          bits 3:0, mask 0x0F

         └── bit 7, mask 0x80:  1 = IN   (device → host)
                                0 = OUT  (host → device)

Bits 3:0 are the endpoint number, masked with 0x0F. Bit 7 is the direction, masked with 0x80. Bits 6:4 are unused.

The two example addresses from Chapter 7.4 §2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  0x81 = 1000 0001    number 1, DIR=1  →  endpoint 1 IN   (device → host)
  0x01 = 0000 0001    number 1, DIR=0  →  endpoint 1 OUT  (host → device)

Same number. Different endpoints. Different buffers, different status bits, different everything — and Chapter 9.4 explains why the direction names are what they are, which is not obvious.

3. What the Decoder Must Do

Four jobs, and the last two are where decoders go wrong.

Extract the number. Mask bits 3:0.

Extract the direction. Test bit 7.

Validate. Not every address names an endpoint this device has. A host can send a transaction to an endpoint the device never declared — because a host can be wrong, because a transaction can be corrupted, or because the device is being probed. The decoder must be able to say no.

Refuse rather than alias. This is Chapter 7.2 §7's conclusion arriving in a new place: an unimplemented selector must produce an unmistakable rejection, not a plausible-looking answer. An endpoint decoder that maps an unknown address onto a real endpoint delivers a host's data into the wrong buffer.

A diagram of endpoint address decoding. An eight-bit endpoint address is split into two parts: bit seven, the direction, and bits three to zero, the endpoint number. The direction bit selects between two independent tables, one for IN endpoints and one for OUT endpoints, each holding which endpoint numbers the device implements. The endpoint number indexes into the selected table. The result is combined with the currently enabled mask from the configuration state, and produces either a valid selection naming one endpoint or one of three distinct refusals: the endpoint is not implemented, it is implemented but not enabled by the active configuration, or the reserved bits were set.Address byteone byte, five bitsmeaningfulBit 7 — directionselects WHICH tableBits 3:0 — numberindexes WITHIN itIN tableimplemented ∧ enabledOUT tableimplemented ∧ enabledSelectionone endpoint, or nothingThree refusalsunimplemented · not enabled· reservedmask 0x80mask 0x0F= 1= 0indexindexhithitmiss12
Figure 1 — the address is split, not looked up as a unit. The direction bit chooses which of two independent tables is consulted, and the four-bit number indexes within it — which is why an address that fails either test must produce a refusal rather than a nearby answer.

4. The Decoder, as RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// usb_ep_decode
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models the
// translation from an endpoint address to a selected endpoint, and the
// rejection of addresses this device does not implement.
//
// WHAT IT MODELS. Section 2's encoding -- four bits of number, one bit of
// direction -- and section 3's four jobs, with an explicit decision about
// the reserved bits.
//
// WHAT IT DOES NOT MODEL. Where the address came from: extracting it from a
// token is Module 11's, and the transaction it belongs to is Module 12's.
// Nor endpoint state (Chapter 9.1's record), buffers (Chapter 9.5), what
// happens after a successful decode, or what the device answers when the
// decode fails -- that response is a protocol behaviour owned by Modules
// 12 and 13, and this block only reports that the decode did not resolve.
//
// COMBINATIONAL BY CHOICE: a mask, a comparison and a table lookup, with no
// reason to add a cycle. The consumer registers the result when it captures
// a transaction.
// ─────────────────────────────────────────────────────────────────────────
package usb_ep_addr_pkg;

  // Chapter 7.4's masks, named rather than written inline. A magic 0x0F in
  // the middle of an expression is a constant nobody can grep for.
  localparam logic [7:0] EP_NUM_MASK = 8'h0F;   // bits 3:0
  localparam logic [7:0] EP_DIR_MASK = 8'h80;   // bit 7
  localparam logic [7:0] EP_RSVD_MASK = 8'h70;  // bits 6:4, unused

  localparam int unsigned EP_NUM_BITS = 4;
  localparam int unsigned N_EP_NUM    = 16;     // 0 .. 15

  typedef enum logic {
    EP_DIR_OUT = 1'b0,   // host -> device   (Chapter 9.4 explains the names)
    EP_DIR_IN  = 1'b1    // device -> host
  } ep_dir_e;

  typedef struct packed {
    logic                    valid;   // this device implements it AND it is
                                      // currently usable
    ep_dir_e                 dir;
    logic [EP_NUM_BITS-1:0]  num;
  } ep_select_t;

endpackage

module usb_ep_decode
  import usb_ep_addr_pkg::*;
#(
  // Which endpoints this device implements, as two bit-masks indexed by
  // endpoint number. Sourced from the DESIGN, not from a second hand-written
  // list -- Chapter 7.4 section 6's requirement, and the reason a checker
  // comparing two hand-written tables verifies nothing.
  parameter logic [N_EP_NUM-1:0] IMPL_IN  = 16'b0000_0000_0000_0111,
  parameter logic [N_EP_NUM-1:0] IMPL_OUT = 16'b0000_0000_0000_0011,

  // RESERVED-BIT POLICY, stated rather than assumed (section 3's callout).
  // 0 = ignore bits 6:4, which is the common and tolerant choice.
  // 1 = reject any address with a nonzero reserved field, which surfaces
  //     corrupted or malformed addresses instead of acting on them.
  parameter bit STRICT_RESERVED = 1'b0
)(
  input  logic [7:0]  ep_addr,

  // Which endpoints the ACTIVE CONFIGURATION has enabled. Chapter 8.5
  // established that endpoints operate only while the device is Configured,
  // and Chapter 9.1 section 6 that enabled must be a subset of implemented.
  // Both masks arrive here; this block enforces the containment rather than
  // assuming it.
  input  logic [N_EP_NUM-1:0] enabled_in,
  input  logic [N_EP_NUM-1:0] enabled_out,

  output ep_select_t  sel,
  output logic        reject_unimplemented,  // no such endpoint in silicon
  output logic        reject_not_enabled,    // exists, but not in this config
  output logic        reject_reserved        // malformed reserved field
);

  logic [EP_NUM_BITS-1:0] num;
  ep_dir_e                dir;
  logic                   impl, en, rsvd_bad;

  always_comb begin
    num      = ep_addr[EP_NUM_BITS-1:0];
    // Explicitly from the mask rather than from ep_addr[7], so the constant
    // and the code cannot drift apart. if/else rather than a ternary: a
    // conditional between two enum literals is not directly assignable to an
    // enum-typed target without an explicit cast, and the cast obscures it.
    if ((ep_addr & EP_DIR_MASK) != 8'h00) dir = EP_DIR_IN;
    else                                  dir = EP_DIR_OUT;
    rsvd_bad = STRICT_RESERVED && ((ep_addr & EP_RSVD_MASK) != 8'h00);

    // Indexed by number, selected by direction -- section 2's two-array
    // structure, in hardware.
    impl = (dir == EP_DIR_IN) ? IMPL_IN[num]    : IMPL_OUT[num];
    en   = (dir == EP_DIR_IN) ? enabled_in[num] : enabled_out[num];

    // THE DEFAULT IS INVALID, and every failing path leaves it that way.
    // Chapter 7.2 section 6's discipline: the tempting mistake is a default
    // arm that resolves to a real endpoint so that "something sensible"
    // happens, which is how a host's data reaches the wrong buffer.
    sel.valid = 1'b0;
    sel.dir   = dir;
    sel.num   = num;

    reject_reserved        = rsvd_bad;
    reject_unimplemented   = !rsvd_bad && !impl;
    // An endpoint that exists but is not enabled is a DIFFERENT condition
    // from one that does not exist, and they are reported separately: a
    // host retrying after configuring the device is sensible in one case
    // and pointless in the other.
    reject_not_enabled     = !rsvd_bad && impl && !en;

    if (!rsvd_bad && impl && en) sel.valid = 1'b1;
  end

endmodule

Purpose. To turn an address into a selected endpoint, or into an explicit refusal with a reason.

Inputs. The address byte, and the enabled masks for both directions.

State. None — combinational, per the header.

Outputs. A selection struct, and three separate rejection reasons.

Hardware implied. A mask, a multiplexer on the direction bit, two 16-bit lookups, and a little combinational logic. Cheap, and shared across all endpoints — Chapter 9.1 §3's figure places it on the shared side deliberately.

Reset. None required.

Assumptions. That the address has already been extracted from a transaction by Module 11; that IMPL_IN/IMPL_OUT come from the design rather than from a second hand-written list; and that the enabled masks come from the block that owns configuration state.

Omissions. Address extraction, endpoint state, buffers, what happens after a successful decode, and what the device answers on a rejection — the last being a protocol behaviour Modules 12 and 13 own.

What DV should verify. That 0x81 and 0x01 select different endpoints; that every implemented and enabled address resolves; that an unimplemented address is refused with a zero selection; that an implemented-but-disabled address is refused with a different reason; that the reserved-bit policy behaves as the parameter says; and that no address produces valid with a number outside the implemented mask.

Three decisions carry the teaching:

  • The rejection reasons are separate. No such endpoint and not enabled in this configuration are different facts, and a host that has just configured a device can act on the difference. Collapsing them into one valid = 0 discards information for no saving.
  • The containment is enforced, not assumed. Chapter 9.1 §6 established enabled ⊆ declared ⊆ implemented. This block checks impl && en rather than trusting en alone, so an enable mask that somehow named an unimplemented endpoint cannot produce a valid selection.
  • The reserved-bit policy is a parameter with a stated default. §3's callout argued both choices are defensible and not knowing which is implemented is not.

5. Mutation Test

Four mutations, run against two verification plans: one that observes only the selection — which is what the rest of the controller consumes, and therefore the plan most people write — and one that also observes why a rejection happened.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  mutation                          selection-only plan   plan incl. the reason
  ─────────────────────────────────────────────────────────────────────────────
  correct decoder                   OK                    OK
  E1  direction bit never read      BROKEN (3)            BROKEN (4)
  E2  no bounds check               BROKEN (4)            BROKEN (8)
  E3  rejection reasons collapsed   OK                    BROKEN (1)
  E4  masks flattened to one        OK                    OK

E1 — the direction bit is never read

The module's defining bug, and it is one line.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dir = EP_DIR_OUT;   // MUTANT E1: the direction bit is never read

Result, probed directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                 correct decoder              E1
  0x81   valid=1  dir=1  num=1        valid=1  dir=0  num=1
  0x01   valid=1  dir=0  num=1        valid=1  dir=0  num=1
                                      >>> MERGED: the same endpoint

0x81 and 0x01 become the same selection. Every transaction for endpoint 1 IN is routed to endpoint 1 OUT's hardware.

Why it works until it does not. A device using only one direction of any given number is unaffected — and that is many devices. The failure needs both directions of the same number in use, which is exactly what a bulk data interface does. Then the two flows share a buffer and corrupt each other.

And the symptom is not silence but corruption: the host's outgoing bytes appear in the device's incoming stream.

E2 — no bounds check

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sel.valid = 1'b1;   // MUTANT E2: always valid

Result. Eight failures. Every address resolves, including endpoints the device does not implement — a transaction to endpoint 9 on a three-endpoint device selects slot 9.

This is Chapter 7.2 §7's aliasing failure in the data path, and it is worse there than in a descriptor lookup: an aliasing lookup returns wrong information, while an aliasing decode moves wrong data into real buffers.

E3 — collapse the two rejection reasons

Result: it depends entirely on what the plan observes.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  selection-only plan      OK (0 errors)      ← survives completely
  plan including reason    BROKEN (1 error)

The selection output is unchanged — both conditions still produce a refusal, and a refusal is what the rest of the controller acts on. Only the reason is lost.

E4 — flatten the two masks into one 32-entry space

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
impl = IMPL_FLAT[{dir, num}];   // MUTANT E4

Result: clean under both plans. Given a correctly constructed flat mask, every address decodes identically.

6. Verification

This chapter's commit point is a transaction has been routed to one endpoint, or refused.

Stimulus. Every implemented and enabled address; both directions of the same number, which is E1's only detector; every unimplemented number, in both directions; an implemented number that the active configuration does not enable; addresses with the reserved bits set, under both parameter settings; and endpoint 0 in both directions, which Chapter 9.3 shows is special.

The stimulus requirement §5 makes non-negotiable: a device that uses both directions of one number. Without it, E1 is invisible — and a testbench built from a device that uses 0x81, 0x02 and 0x83 never generates it.

Observation. The selection and the rejection reason. §5 measured E3 surviving a selection-only plan with zero errors and failing the moment the reason is observed — the two plans differ in exactly one observation, and that is the one.

Reference model. A two-set model: implemented IN, implemented OUT, intersected with the enabled masks. The expected result is set membership, which is a two-line model and worth writing precisely because the DUT's version is a mask lookup and the model's is a set — two different representations of one fact, which is what makes the comparison meaningful.

Representative coverage — crosses:

  • endpoint number 0 through 15 × direction IN and OUT — all 32 cells
  • implemented × enabled: both, implemented-only, neither
  • reserved bits zero × nonzero, crossed with the policy parameter
  • both directions of the same number, active in the same test
  • a number implemented in one direction and not the other

Negative cases with defined outcomes: an unimplemented address must produce valid = 0 and a zero selection, not a defaulted one; an implemented-but-disabled address must be distinguishable from an unimplemented one; and no address may produce a valid selection outside the implemented mask, whatever the enabled mask says.

7. Debugging: Data in the Wrong Direction

A device's bulk interface works in one direction. Used in both directions simultaneously, data corrupts: bytes the host sent appear in what the device returns.

What does works in one direction rule out? The buffer, the transfer logic, and the endpoint's state machine — all of which handle one direction correctly.

What does corrupts when both are used point at? Something the two directions share. They are supposed to share almost nothing: Chapter 9.1 established that 0x81 and 0x01 are different endpoints with different buffers.

So what could make them share? A decode that maps them to the same slot. §5's E1.

What is the first observation? The decoded selection for both addresses. If 0x81 and 0x01 produce the same (dir, num) pair — or the same buffer index downstream — the decoder is merging them.

What would a protocol analyser show? Correct traffic. The host addressed 0x81 and 0x01 exactly as it should, and the device responded to both. Nothing on the bus is wrong, which is why this must be found inside the controller.

And the signature? Works singly, corrupts in combination, with a correct bus trace points at a shared resource that should not be shared. That is a different search from does not work at all, and starting from the decode rather than the data path is what makes it quick.

8. Common Misconceptions

9. Reason It Through

A reviewer proposes replacing the two 16-bit implemented masks with a single 32-bit mask indexed by the full address, on the grounds that it is simpler and §5 measured it as equivalent.

Is the equivalence claim correct? Yes — §5's E4 measured it. Given a correctly constructed flat mask, every address decodes identically.

So is the proposal right? It depends on something the equivalence does not address: what the representation makes obvious to the next reader.

What does the flat version obscure? That the index is a composite. IMPL_FLAT[17] is endpoint 1 IN, and nothing about the expression says so. A reader has to know that bit 4 of the index is the direction, which is knowledge held outside the code.

Does that matter, given it is documented? It matters at the moment someone writes a new expression. The two-array form makes indexed by number, selected by direction the only thing you can write; the flat form makes it one of several, and the others compile.

Is there a case for the flat version? Yes — a design where the 32 slots genuinely are one uniform resource, allocated dynamically rather than fixed per direction. Then the flat space is the truth and splitting it would be the misleading choice.

And the principle? Choose the representation that matches the structure of the thing, not the one with fewer characters. Where direction and number are independent, two arrays say so. Where 32 slots are one pool, one array says so. The equivalence measured in §5 means the choice is free of behavioural risk — which is exactly when legibility should decide it.

10. Understanding Check

11. Summary

An endpoint address is a pair, not a number. One byte carries four bits of endpoint number in bits 3:0 and one bit of direction in bit 7, and the pair selects one piece of hardware. 0x81 and 0x01 share a number and are different endpoints with different buffers and different state — which is why host software holds two arrays of sixteen rather than one array of thirty-two.

The decoder has four jobs: extract the number, extract the direction, validate, and refuse rather than alias. The last is Chapter 7.2's conclusion arriving in the data path, where it matters more — a descriptor alias returns wrong information, and an endpoint alias moves a host's data into the wrong buffer.

The reserved bits are a decision, not a detail. Ignoring them is common and defensible; rejecting them is also defensible; not knowing which was implemented is not.

§5 measured four mutations and two of them survived, meaning opposite things. Ignoring the direction bit merges both directions of a number and works until both are used at once — the bulk-interface case, where the symptom is wrong data rather than no data, with a correct bus trace. Dropping the bounds check aliases into real buffers. But collapsing the rejection reasons survives because nothing was observing them, and flattening the masks survives because it is genuinely equivalent.

Which is the verification lesson worth keeping: when a mutation survives, ask whether the difference is unobservable or merely unobserved. The first is a free design choice; the second is a gap in the plan wearing the same clothes.

12. What Comes Next

The address has a four-bit number, and four bits is a decision nobody has justified yet.

Chapter 9.3 asks why sixteen, what endpoint zero is doing that makes it different from the other fifteen, and what a slot actually costs — because Chapter 9.1 measured an endpoint's record at eighteen bits and that turns out to be the cheap part by a wide margin.

It is also where the containment invariant stops being a diagram and becomes an elaboration-time check: whether a configuration's endpoints fit inside the silicon's is a fact about two constants, and a fact about two constants should never be discovered in simulation.

Browse the full path on the USB tutorials index.

Continue learning

Standards & specifications

Governing standard
USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)

Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.

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 USB curriculum.