USB · Module 9
Endpoint Buffers
Why an endpoint needs storage, why one buffer forces a choice between filling and being collected, and the ping-pong structure that removes it — with the occupancy-versus-flags invariant a single comparison breaks.
Chapter 9.4 ended with a problem it could not solve: an IN endpoint must be filled in advance, because a device cannot announce that it has become ready — and nothing tells it when collection will happen.
This chapter is the answer, and it is where Chapter 9.1's claim that the buffer dominates an endpoint's cost gets paid:
A buffer exists to decouple two schedules that have no relationship to each other — the device's, which produces and consumes data when its application does, and the host's, which collects and delivers it when the bus schedule allows.
The interesting structure is two buffers, and the interesting bug is a single comparison.
1. Why Storage Is Required
Not obvious until you try to remove it.
Suppose an IN endpoint had no buffer and the device produced data directly onto the bus when asked. The device would have to have the data at the instant the host asks, which it cannot arrange, because it does not know when that is — Chapter 9.4 §2's asymmetry. Every poll arriving at the wrong moment finds nothing.
Suppose an OUT endpoint had no buffer and the device consumed data directly as it arrived. The device's application would have to be ready to consume at the instant the host sends, which is equally outside its control.
So storage is what makes the two schedules independent. The device writes when it has data; the host collects when the bus allows; the buffer absorbs the difference.
And that framing tells you how to size it. A buffer must hold what accumulates between collections — which depends on the production rate and on the worst-case interval between the host's visits. Chapter 9.1 §5 made the point and it is worth repeating because it is the sizing error people make: that interval depends on what else is on the bus, not on this device.
2. One Buffer Forces a Choice
Now the structure, starting from the simplest thing that could work.
One buffer, one endpoint. The device fills it; the host empties it. What happens while the host is collecting?
The device cannot fill it. The data being transmitted is in the buffer, and overwriting it would corrupt the transfer in flight. So the device waits.
And what happens right after a collection? The buffer is empty, the device starts filling it, and the host may ask again before the fill completes — finding a buffer that is neither the old data nor the new, and being told not ready by a device that has data it simply cannot present yet.
That is the single-buffer stall, and it is a real throughput limit rather than a corner case. The device alternates between filling and being collected, and during each it cannot do the other.
3. Ping-Pong Removes the Choice
Two banks. The device owns one, the host owns the other, and they swap.
While the host collects bank B, the device fills bank A. When the host finishes and the device has finished, the roles exchange: the host collects A and the device fills B.
The device never waits for the host, provided it can fill one bank in the time the host takes to collect the other. That proviso is the whole design question, and it is a rate comparison rather than a correctness one.
The cost is exactly double the storage, which Chapter 9.3 §4 counted: roughly 1,000 bits for a double-buffered 64-byte endpoint against 512 for a single one. That is the dominant cost of an endpoint, and it is why devices implement few of them.
4. The Structure to Get Right
Three pieces of state, and the relationships between them are where the bugs live.
Which bank the device owns, and by implication which the host owns. One bit.
How much is in each bank. An occupancy count per bank — how many bytes have been written into it.
Whether each bank is full or empty. Which is derived from occupancy, and Chapter 8.5 §3's argument applies unchanged: a flag stored separately from the count it summarises is a flag that will eventually disagree with it.
The invariant that ties them together:
full ⟺ occupancy = capacity
empty ⟺ occupancy = 0Stated as an equivalence, deliberately. Two implications would allow a design where the flag is set without the condition or the condition without the flag, and §7 measures exactly one of those being introduced by a single character.
5. The Ping-Pong Buffer, as RTL
The module's densest block, and every line of the swap logic is load-bearing.
// ─────────────────────────────────────────────────────────────────────────
// usb_ep_pingpong
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models a
// two-bank endpoint buffer with ownership handoff, and the occupancy /
// flag relationship of section 4.
//
// WHAT IT MODELS. Section 3's structure: the device writes one bank while
// the bus side reads the other, and the banks swap when both are done.
// Occupancy is counted per bank and the full/empty flags are DERIVED from
// it -- section 7 measures what a stored flag costs.
//
// WHAT IT DOES NOT MODEL. Which endpoint this belongs to (Chapter 9.2),
// direction steering (Chapter 9.4 -- this is written for the IN direction,
// device-writes/bus-reads, and the OUT direction is the same structure with
// the two ports exchanged), the transaction that drives the bus side
// (Module 12), packet framing (Module 11), what the device ANSWERS when no
// bank is ready (Module 12), or transfer-type-specific behaviour
// (Module 10).
//
// ON THE MEMORY. Written as two arrays rather than one with an extra
// address bit, for the reason Chapter 9.2 section 9 gives about the two
// endpoint tables: the banks are independently owned, and one array with a
// bank bit in the index obscures that. A real design may well use one
// dual-port RAM with the bank as an address bit; that is an implementation
// of this structure, not a different structure.
// ─────────────────────────────────────────────────────────────────────────
module usb_ep_pingpong #(
// Maximum packet size for this endpoint -- from the descriptor
// (Chapter 7.4). The buffer must be at least this large, which is the
// promise Chapter 7.4 section 1 says the hardware has to keep.
parameter int unsigned MAX_PACKET = 64
)(
input logic clk,
input logic rst_n,
// A bus reset empties both banks: data staged for a host that has just
// reset the device is data the host is no longer expecting
// (Chapter 8.3 section 6).
input logic bus_reset,
// ── Device side: writes into the bank it owns ──────────────────────────
input logic [7:0] dev_wr_data,
input logic dev_wr_en,
// The device has finished a packet: hand this bank to the bus side.
input logic dev_commit,
output logic dev_can_write, // the device's bank has room
// ── Bus side: reads from the bank the device handed over ───────────────
output logic [7:0] bus_rd_data,
input logic bus_rd_en,
// The bus side has finished: return this bank to the device.
input logic bus_release,
output logic bus_has_packet, // a committed bank is waiting
// Exported for status and for the composition check of section 6.
output logic [$clog2(MAX_PACKET+1)-1:0] dev_occupancy,
output logic [$clog2(MAX_PACKET+1)-1:0] bus_occupancy
);
localparam int unsigned CNT_W = $clog2(MAX_PACKET+1);
// Two banks, independently owned (see the note above).
logic [7:0] bank0 [0:MAX_PACKET-1];
logic [7:0] bank1 [0:MAX_PACKET-1];
// Which bank the DEVICE currently owns. The bus side owns the other.
logic dev_bank;
// Per-bank occupancy, and per-bank "committed" -- a bank that the device
// has finished with and handed over.
logic [CNT_W-1:0] occ [0:1];
logic commit [0:1];
logic [CNT_W-1:0] bus_rd_ptr;
// ── DERIVED, never stored (section 4, and Chapter 8.5 section 3) ────────
// The device may write if the bank it owns is not committed and not full.
assign dev_can_write = !commit[dev_bank] && (occ[dev_bank] < MAX_PACKET[CNT_W-1:0]);
// The bus side has something if the bank it owns has been committed.
assign bus_has_packet = commit[~dev_bank];
assign dev_occupancy = occ[dev_bank];
assign bus_occupancy = occ[~dev_bank];
assign bus_rd_data = (~dev_bank) ? bank1[bus_rd_ptr] : bank0[bus_rd_ptr];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dev_bank <= 1'b0;
occ[0] <= '0;
occ[1] <= '0;
commit[0] <= 1'b0;
commit[1] <= 1'b0;
bus_rd_ptr <= '0;
end else if (bus_reset) begin
// Both banks emptied, ownership reset. Note occupancy AND commit are
// cleared together: a committed bank with zero occupancy would be a
// packet of no bytes that the bus side would dutifully present.
dev_bank <= 1'b0;
occ[0] <= '0;
occ[1] <= '0;
commit[0] <= 1'b0;
commit[1] <= 1'b0;
bus_rd_ptr <= '0;
end else begin
// ── Device side ────────────────────────────────────────────────────
if (dev_wr_en && dev_can_write) begin
if (dev_bank) bank1[occ[1]] <= dev_wr_data;
else bank0[occ[0]] <= dev_wr_data;
occ[dev_bank] <= occ[dev_bank] + 1'b1;
end
// ── The handoff ────────────────────────────────────────────────────
// The device hands its bank over and takes the other -- but ONLY if
// the other is free. If the bus side has not released its bank, the
// device has nowhere to go and must wait. That wait is the ping-pong
// structure's remaining stall, and it is the one the sizing argument
// in section 3 is about.
if (dev_commit && !commit[dev_bank]) begin
commit[dev_bank] <= 1'b1;
if (!commit[~dev_bank]) dev_bank <= ~dev_bank;
end
// ── Bus side ───────────────────────────────────────────────────────
if (bus_rd_en && bus_has_packet && (bus_rd_ptr < occ[~dev_bank]))
bus_rd_ptr <= bus_rd_ptr + 1'b1;
if (bus_release && bus_has_packet) begin
// Returning the bank: clear its occupancy and its commit together.
occ[~dev_bank] <= '0;
commit[~dev_bank] <= 1'b0;
bus_rd_ptr <= '0;
// If the device was waiting for a free bank, it now has one.
if (commit[dev_bank]) dev_bank <= ~dev_bank;
end
end
end
endmodulePurpose. To let the device fill one bank while the bus side drains the other, so neither waits for the other under normal rates.
Inputs. Clock, local reset, bus reset; the device's write port and commit; the bus side's read port and release.
State. Two banks of MAX_PACKET bytes, a bank-ownership bit, two occupancy counters, two commit flags, and a bus-side read pointer.
Outputs. The read data, both readiness flags, and both occupancies for status and checking.
Hardware implied. 2 × MAX_PACKET bytes of storage — the dominant cost — plus two small counters, a pointer and a handful of flags. For a 64-byte endpoint that is 1,024 bits of storage against about 25 bits of control.
Reset. The local reset and a bus reset both empty both banks and return ownership to bank 0. Occupancy and commit are cleared together, because a committed bank with zero occupancy is a zero-byte packet the bus side would present as real.
Assumptions. That dev_commit and bus_release are single-cycle and qualified; that the bus side does not read past the occupancy it was given; that the device does not write while dev_can_write is low; and that this is the IN direction — the OUT direction is the same structure with the ports exchanged.
Omissions. Endpoint identity, direction steering, transactions, packets, the device's response when no bank is ready, and transfer types.
What DV should verify. That the derived flags always match the occupancies; that a commit with no free bank stalls rather than corrupting; that a bus reset empties both banks and clears both commits; that occupancy never exceeds capacity; that the bus side never reads a bank the device owns; and — the boundary that matters — behaviour at occupancy 0, 1, MAX_PACKET-1 and MAX_PACKET.
6. The Composition Check
The occupancy-versus-flags relationship spans the counters and the derived signals, and Chapter 8.4's technique applies.
// ─────────────────────────────────────────────────────────────────────────
// Classification: TEACHING ASSERTIONS about the ping-pong invariants.
// B-properties are SAFETY; P-properties are PROGRESS. Section 7 measures
// why both are needed -- a buffer that accepts nothing satisfies every
// safety property here.
// ─────────────────────────────────────────────────────────────────────────
// B1 -- BOUNDS. Occupancy never exceeds capacity. This is the property a
// single wrong comparison in dev_can_write defeats (section 7), and its
// failure means the device wrote past the end of a bank.
property p_occupancy_bounded;
@(posedge clk) disable iff (!rst_n)
(dev_occupancy <= MAX_PACKET) && (bus_occupancy <= MAX_PACKET);
endproperty
assert property (p_occupancy_bounded);
// B2 -- THE DERIVED FLAG. The device may write exactly when its bank is
// uncommitted and has room. Written as an equivalence rather than an
// implication: a flag that is low when writing WOULD be legal is a stall,
// and a flag that is high when it would not be is corruption. Both are
// defects and only an equivalence forbids both.
property p_can_write_is_derived;
@(posedge clk) disable iff (!rst_n)
dev_can_write == (!commit[dev_bank] && (dev_occupancy < MAX_PACKET));
endproperty
assert property (p_can_write_is_derived);
// B3 -- OWNERSHIP. The bus side never sees a bank the device still owns.
// This is the property that makes the structure safe at all: if it can
// fail, the host can read bytes the device is midway through writing.
property p_bus_reads_only_committed;
@(posedge clk) disable iff (!rst_n)
(bus_rd_en && bus_has_packet) |-> commit[~dev_bank];
endproperty
assert property (p_bus_reads_only_committed);
// B4 -- NO WRITE WITHOUT PERMISSION. Occupancy only rises when the device
// was allowed to write.
property p_no_unpermitted_write;
@(posedge clk) disable iff (!rst_n)
(dev_occupancy > $past(dev_occupancy)) |-> $past(dev_wr_en && dev_can_write);
endproperty
assert property (p_no_unpermitted_write);
// B5 -- RESET. A bus reset empties both banks AND clears both commits, as
// one conjunction. Chapter 8.4's I4: splitting this into two properties
// loses the case where one holds and the other does not -- here, a
// committed bank with zero occupancy, which the bus side would present as
// a zero-byte packet.
property p_bus_reset_empties_both;
@(posedge clk) disable iff (!rst_n)
bus_reset |=> (dev_occupancy == 0 && bus_occupancy == 0
&& !commit[0] && !commit[1] && !bus_has_packet);
endproperty
assert property (p_bus_reset_empties_both);
// P1 -- PROGRESS. Every safety property above is satisfied by a buffer that
// never accepts a byte. This is what forbids that: if a write arrives when
// the CONDITION for accepting it holds, occupancy must rise.
//
// NOTE THE ANTECEDENT. It is the condition -- uncommitted and not full --
// and deliberately NOT `dev_can_write`. Gating a progress property on the
// permission flag makes it vacuous exactly when the flag is wrong, which
// is the case it exists to catch. Section 7 measured that: with the flag
// tied low and B2 weakened to an implication, a version of P1 written
// against `dev_can_write` fired on nothing at all.
property p_write_is_accepted;
@(posedge clk) disable iff (!rst_n)
(dev_wr_en && !commit[dev_bank] && (dev_occupancy < MAX_PACKET)
&& !bus_reset && !dev_commit)
|=> (dev_occupancy == $past(dev_occupancy) + 1);
endproperty
assert property (p_write_is_accepted);
// P2 -- PROGRESS. A committed bank becomes visible to the bus side. Without
// this, a design that commits and never hands over satisfies everything.
property p_commit_becomes_visible;
@(posedge clk) disable iff (!rst_n)
(dev_commit && !commit[dev_bank] && !bus_reset) |=> bus_has_packet;
endproperty
assert property (p_commit_becomes_visible);B2 is the equivalence this chapter is about. Chapter 9.4 §4 made the same argument about a different signal: an implication in one direction permits a stall, and in the other permits corruption, and only the equivalence forbids both.
B5's conjunction is Chapter 8.4 §3's I4, and the case it protects is specific: a bank whose commit survives a reset while its occupancy is cleared is a zero-byte packet the bus side would faithfully present.
And P1's antecedent is the chapter's sharpest lesson. Progress properties exist because safety properties are all satisfied by a buffer that does nothing — Chapter 8.3 §9's conclusion. But §7 measured a progress property that was itself defeated by the same mutation, because it was gated on the permission flag rather than on the condition. A progress property gated on a design signal is only as live as that signal.
7. Mutation Test
Five mutations, and the last one was run against three property sets — because the result of the first two runs changed one of the properties above.
mutation fires
─────────────────────────────────────────────────────────────
correct design (nothing)
B-M1 `<=` instead of `<` B1 and B2, 32 violations
B-M2 store the flag, not derive it B2
B-M3 swap without checking free B4
B-M4 reset clears occupancy only B5
B-M5 accept nothing B2B-M1 — <= instead of <
One character in the write permission.
Result. B1 and B2 both fire, 32 property violations across the run. The device is permitted one write past the end of the bank: occupancy reaches MAX_PACKET + 1 and the write indexes one byte beyond the array.
And the failure is silent in the data. The extra byte lands outside the bank — in an adjacent structure, or nowhere, depending on how the memory is implemented — so the packet the host collects is correct. A device whose endpoint works perfectly while quietly overwriting something it does not own.
B-M2 — store the flag instead of deriving it
Result. B2 fires, and the timing is the point: on the cycle after a bus release, when the bank has been returned empty but the stored flag has not caught up. The device is told it cannot write to a bank that is entirely free.
A stall rather than corruption, which is why it survives casual testing — throughput is slightly lower and the data is perfectly correct. Chapter 8.5 §3's argument for deriving, measured on a different signal.
B-M3 — swap banks without checking the other is free
Result: B4 fires — and notably B3 does not, which is worth working through because it was the predicted catcher.
The device takes ownership of a bank the bus side still holds. But dev_can_write is derived from commit[dev_bank], and the bank it just grabbed is committed — so the device is immediately unable to write to it. No corrupt write occurs.
What does break is the reported occupancy: dev_occupancy now reads a different bank's counter, so it changes with no write behind it, and B4 — occupancy only rises with permission — catches exactly that.
So the structure partially defends itself, and the catch comes from a property about provenance rather than the one about ownership. B3's antecedent is not violated because bus_has_packet still points at a committed bank; the ownership is scrambled without B3's specific condition ever being false. A defect being caught by a property other than the one written for it is worth noticing, because it means the intended property has not been exercised.
B-M4 — clear occupancy on bus reset but not the commits
Result. B5 fires. Both banks report zero occupancy while one is still committed, so bus_has_packet stays high and the bus side is offered a zero-byte packet.
Exactly the case the conjunction protects. Two separate properties — occupancy is zero and commits are cleared — would both pass on a design where only the first happened.
B-M5 — accept nothing
Tie the write permission low.
Result, measured against three property sets:
fires
B2 as an EQUIVALENCE B2
B2 weakened to an implication nothing at all
implication + P1 corrected P1The middle row is the finding, and it is worse than expected. With B2 written as the natural-seeming implication — permission implies the condition — a buffer that accepts nothing passes every property in the set, safety and progress alike.
Why does progress not catch it? Because P1's antecedent, as first written, was dev_wr_en && dev_can_write. The mutation ties dev_can_write low, so the antecedent is never true and P1 is vacuous — gated on precisely the signal whose incorrectness it exists to detect.
8. Verification
This chapter's commit point is data crossed between two schedules without either corrupting or stalling the other.
Stimulus. Writes at occupancy 0, 1, MAX_PACKET-1 and MAX_PACKET; a commit with the other bank free and with it held; a release while the device is waiting; a bus reset with one bank committed, with both empty, and mid-read; back-to-back commits; and a read attempted with no committed bank.
The boundary requirement is not advisory here. §7's B-M1 is one character and is visible only at occupancy exactly MAX_PACKET. A test writing "a packet's worth" of data without landing precisely on the boundary never generates it.
Observation. Both occupancies, both flags, the ownership bit and the data. §7's B-M1 produces correct packet data while corrupting something outside the buffer — so a data-only scoreboard reports it as passing.
Reference model. Two queues and an ownership bit. Push on an accepted write, hand over on commit, pop on read, empty on release. The model is small and its value is that it tracks ownership, which a data-only comparison does not represent at all.
Scoreboard thinking. Comparing emitted bytes against expected bytes is necessary and insufficient: it cannot see B-M1's out-of-bounds write, B-M2's stall, or B-M3's ownership violation until the corruption happens to land in data the test reads. Ownership and occupancy have to be compared as well as content.
Representative coverage — crosses:
- occupancy at 0 × 1 ×
MAX_PACKET-1×MAX_PACKET, crossed with a write attempt - commit with the other bank free × committed
- release with the device waiting × not waiting
- bus reset at: idle, one bank committed, mid-read, mid-fill
- read attempted with a committed bank × with none
Negative cases with defined outcomes: a write at full capacity must not be accepted; a commit with no free bank must stall rather than take the other bank; a read with no committed bank must not return data; and a bus reset must clear occupancy and commit together.
9. Debugging: Rare Corruption Under Load
A bulk IN endpoint works. Under sustained load, roughly one packet in several thousand contains a few bytes of data from an earlier packet.
What does works, except rarely rule out? Everything structural. The decode, the direction, the enable and the buffer's basic operation are all fine — a systematic fault would corrupt every packet.
What does bytes from an earlier packet point at? A bank being read while it is being rewritten, which is §7's B-M3: the ownership handoff taking a bank the bus side still owns.
Why is it rare? Because it needs the device to finish filling while the bus side is mid-read of the other bank — a timing coincidence that only occurs when both sides are running near their rates, which is what under sustained load means.
What is the first observation? The ownership bit against the commit flags, at the moment of a handoff. If dev_bank moves while the other bank is still committed, the guard is missing or wrong.
What would a protocol analyser show? A packet with plausible content. Nothing is malformed — the length is right, the framing is right, and some bytes are stale. This cannot be found from the bus.
And the alternative hypothesis worth eliminating? §7's B-M1 — a write one byte past the end. It produces a different signature: the packet data is correct and something adjacent is corrupted, so if the reported corruption is inside the packet, B-M3 is the candidate and if it is elsewhere in the device, B-M1 is.
The discipline: rare, under load, stale data inside otherwise valid packets is an ownership signature. It says two parties touched one resource, and it points at the handoff rather than at either party.
10. Common Misconceptions
11. Reason It Through
A design review proposes a third bank, on the grounds that if two are better than one, three will be better than two.
What did the second bank buy? §3: it removed the device's self-inflicted stall — being unable to fill because the host is reading the only bank. After that change, the device stalls only when it produces two bankfuls faster than the host collects one.
What would a third buy? It absorbs a longer burst. The device could produce three bankfuls ahead of collection instead of two.
So when is that useful? When production is bursty relative to collection — the device occasionally produces faster than the host drains, and then pauses. A third bank rides out the burst.
And when is it useless? When production is sustained above the collection rate. Then the device fills every bank there is and stalls anyway, one bank later. Buffering absorbs bursts; it does not fix a rate mismatch — §5's callout states this and a third bank is the place people rediscover it.
What does it cost? Another MAX_PACKET bytes, which §3 established is the dominant cost of an endpoint. Half again as much storage for the largest component of the design.
How would you decide? Measure the distribution of the gap between host visits. If the tail is long — occasional long gaps, otherwise prompt — a third bank helps. If the mean gap already exceeds the device's fill time, no number of banks helps and the endpoint is simply not being polled fast enough.
And the principle? Buffering converts a rate requirement into a burst requirement, and only up to the amount of buffering you have. The question is never how many banks but what is the distribution of the gap I am absorbing — and that is a measurement, not an architecture decision.
12. Understanding Check
13. Summary
A buffer decouples two schedules with no relationship to each other — the device's application and the host's bus scheduling. That framing also gives the sizing rule: a buffer holds what accumulates between collections, and the worst-case interval between visits is a property of everything else on the bus, not of this device.
One buffer forces a choice. The device cannot fill while the host reads, and the host finds not ready while the device fills. Two banks remove the choice: the device owns one, the bus side owns the other, and they swap — at exactly double the storage, which is the dominant cost of an endpoint.
What ping-pong buys is bounded and worth stating honestly. It removes the device's self-inflicted stall and adds no bandwidth. A device producing two bankfuls faster than the host collects one still waits, because buffering absorbs bursts and does not fix a rate mismatch.
The structure to get right is occupancy, ownership, and derived flags — with full ⟺ occupancy = capacity stated as an equivalence, because §7 measured why. Five mutations:
- One character,
<=for<, permits a write past the end — producing perfectly correct packets while corrupting something outside the bank, which a data-only scoreboard reports as passing. - Storing the flag instead of deriving it tells the device it cannot write to a bank that is entirely free: a stall, not corruption, and therefore survivable in testing.
- Swapping banks without checking the other is free lets the device write a bank the host is reading — the ownership violation the structure exists to prevent, and the one whose field signature is rare, under load, stale bytes inside valid packets.
- Clearing occupancy but not commit on a bus reset offers a zero-byte packet, which is why the reset property is one conjunction rather than two.
- Accepting nothing at all is caught by the equivalence and would have been invisible to every safety property had the derivation been asserted as an implication.
Which is the transferable rule: assert a derived signal as an equality. The natural-seeming implication is weaker for no benefit, and the defect it lets through is the one where the design does nothing.
14. Where This Leaves You
Module 9 is complete. An endpoint is a buffer with an identity and its own state; its identity is a number and a direction that select one of two arrays; the numbers are budgeted, because a slot is dominated by storage; the directions are not mirror images, because only one of them can be asked for something it does not have; and the storage is two banks, because one forces a choice between producing and being collected.
What the module actually built is the vocabulary every later USB chapter needs: a destination that can be addressed, refused, enabled, filled, drained and handed over.
What it deliberately did not build is any notion of what kind of traffic an endpoint carries. Every chapter here has said a transaction arrives and stopped. An endpoint descriptor declares a transfer type (Chapter 7.4), and this module stored it in a record and never used it.
Chapter 10.1 is where that field starts to matter. The four transfer types — control, bulk, interrupt, isochronous — are the answer to a question this module has been carefully not asking: what guarantees does this flow need? An endpoint that must never lose data and an endpoint that must never be late are different requirements, and no amount of buffering satisfies both.
That trade space is the next unresolved problem, and it is the one that decides how the endpoints built here are actually used.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
Endpoint Direction
IN and OUT are named from the host's point of view, so an IN endpoint sends data from the device. Why the host initiates both directions, and why the two directions are not mirror images in hardware.
- Related topic
The Parallel Port
Why presenting eight data lines at once forces an explicit data/strobe/acknowledge handshake, what that costs in timing discipline, a synthesizable teaching FSM that implements it with the assertions that protect it, and why an interface shaped around one peripheral's operational model cannot generalise.
- Related topic
PS/2 Keyboard / Mouse
The dedicated input port where the device supplies the clock and the host receives on someone else's timing. The two-wire open-drain bus, the framed byte, why sampling a foreign clock directly is a real hardware bug, and a synthesizable teaching receiver with its synchroniser, recovery timeout and assertions.
- Related topic
The USB Device
What is left when every decision belongs to the host: the responder discipline, why a device must answer even when it has nothing to say, the split between the USB-facing device controller and the function it exists to provide, and a teaching abstraction with the assertion that protects the ownership model.
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.
