USB · Module 10
Bulk Transfers
Promised nothing and permitted everything: why throughput and guarantee are different axes, and why a fixed-priority arbiter starves an endpoint forever while every safety property passes.
Chapter 10.2 §3 showed the budget as a chain of guarantees: the periodic cap guarantees a non-periodic residue exists, and control traffic is protected within it. Bulk was at the end of that chain, described in three words — whatever survives both.
That is the whole of Bulk's service contract, and it produces an apparent contradiction:
Bulk is promised nothing, and on most buses it moves more data than every other type combined.
Understanding why those are compatible is this chapter, and it turns on a distinction that has come up twice already: throughput and guarantee are different axes.
1. What Bulk Is Not Promised
Start with the absences, because they are the definition.
No reservation. Chapter 10.1 §5 established that periodic types consume budget from configuration until un-configuration. Bulk consumes none — it asks for nothing in advance and is admitted unconditionally.
No service interval. An interrupt endpoint's configuration states how often it wants to be visited. A bulk endpoint states no such thing, and the host owes it no particular visit at any particular time.
No bounded latency. A bulk transfer may be served immediately or may wait, and nothing in the protocol caps the wait. A device that has been ignored for a long time has no complaint to make.
What it does get is correctness. A bulk transfer's data is delivered intact or the error is visible, and a failure can be retried — Chapter 10.1 §2's grid places Bulk firmly in the retry is useful column.
2. Throughput and Guarantee Are Different Axes
The distinction this chapter exists to establish, because the stereotype Bulk is slow comes from collapsing them.
Throughput is how much data moves per unit time, averaged. It depends on how much capacity is available and how efficiently the flow uses it.
A guarantee is a promise about a particular moment. It says you will be served within this interval, or this much capacity is yours.
They are independent, and all four combinations exist:
| Has a guarantee | Has no guarantee | |
|---|---|---|
| High achievable throughput | Isochronous — reserved, and reservations can be large | Bulk — takes the residue, which is usually most of the bus |
| Low achievable throughput | Interrupt — reserved, but the reservation is typically small | Control — protected, but management traffic is rare by nature |
Bulk sits in the top-right, and the stereotype puts it in the bottom-right.
The practical consequence is the one that matters in design: a device needing to move a lot of data should use Bulk precisely because it has no guarantee. Asking for a guarantee means asking for a reservation, reservations are capped, and a reservation large enough to carry bulk-scale data would consume the cap on its own — and then be wasted whenever the device was idle.
3. What Bulk Gives Up, and When It Hurts
The trade has a cost, and being precise about when it is paid matters more than the trade itself.
On a quiet bus, Bulk costs nothing. The residue is large, service is prompt, and a bulk endpoint is effectively served on demand.
On a busy bus, latency becomes unbounded and variable. Not merely long — variable, which is worse for anything downstream that has to size a buffer or a timeout.
And the sources of contention are worth separating, because they behave differently:
- Periodic traffic elsewhere on the bus shrinks the residue by a fixed amount for as long as those devices are configured. Predictable, and visible in advance from what is attached.
- Other bulk traffic shares the residue dynamically. Unpredictable, and it is what §4's arbitration is about.
- Control traffic takes its protected share, which is small and rare.
The one a device engineer controls is the second, and it is the one that produces the module's most instructive failure.
4. Sharing the Residue
Several bulk endpoints — on one device or across devices — compete for the same leftover capacity. Something decides the order, and the decision has consequences that outlast any single transfer.
The naive answer is fixed priority. Serve the lowest-numbered pending endpoint. It is trivially simple, it is what a first implementation produces, and it has a failure mode that is total rather than gradual.
A continuously-busy high-priority endpoint starves every lower one, forever. Not slowly — never. And the starved endpoint is not broken, is not reporting an error, and has data waiting the whole time.
Round-robin fixes it by remembering where the last grant went and resuming from the next candidate. The cost is one register.
5. The Opportunistic Arbiter, as RTL
// ─────────────────────────────────────────────────────────────────────────
// usb_bulk_arbiter
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models the
// selection of one bulk endpoint from several pending ones, and the
// precedence of a periodic obligation over all of them.
//
// WHAT IT MODELS. Section 3's two rules: opportunistic traffic runs only
// when no periodic obligation is due, and among competing opportunistic
// requesters the choice is round-robin so that none is starved.
//
// WHAT IT DOES NOT MODEL -- AND THIS LIST MATTERS. This is NOT a USB host
// scheduler. It does not model frames or microframes, bandwidth accounting,
// the admission of periodic reservations, transaction boundaries (Module
// 12), packets (Module 11), split transactions, multiple devices, or the
// order in which a real host walks its schedule. It models ONE decision --
// which requester is selected when an opportunity exists -- because that
// decision is where starvation is introduced, and starvation is the lesson.
//
// A real host controller's scheduling is a substantially larger subject
// that later modules and Module 22 own.
// ─────────────────────────────────────────────────────────────────────────
module usb_bulk_arbiter #(
parameter int unsigned N_REQ = 4
)(
input logic clk,
input logic rst_n,
// A service opportunity exists this cycle. In a real controller this
// arrives from the scheduler; here it is simply "there is room to do
// something".
input logic opportunity,
// A periodic endpoint is owed service now. Section 3: periodic
// obligations are reservations that have come due, and opportunistic
// traffic must not displace them.
input logic periodic_due,
// Which opportunistic requesters have data waiting.
input logic [N_REQ-1:0] req,
output logic [N_REQ-1:0] grant, // one-hot, or zero
output logic grant_valid,
output logic periodic_win // the opportunity went to periodic
);
localparam int unsigned PTR_W = (N_REQ > 1) ? $clog2(N_REQ) : 1;
// WHERE THE LAST GRANT WENT. This one register is the entire difference
// between round-robin and fixed priority (section 4), and section 6
// measures what its absence costs.
logic [PTR_W-1:0] rr_ptr;
logic [N_REQ-1:0] sel;
logic found;
// ── Round-robin selection ───────────────────────────────────────────────
// Walk the requesters starting from rr_ptr and take the first pending
// one. Written as an explicit ordered search rather than as a rotated
// priority encoder because the ordering is the point: a reader can see
// that the starting position moves, which is what prevents starvation.
always_comb begin
int unsigned idx; // block-scoped: no lifetime override, so
// every tool in the flow accepts it
sel = '0;
found = 1'b0;
idx = 0;
for (int unsigned i = 0; i < N_REQ; i++) begin
// (rr_ptr + i) modulo N_REQ -- the rotation, and the only thing that
// separates this from a plain priority encoder.
idx = (rr_ptr + i) % N_REQ;
if (!found && req[idx]) begin
sel[idx] = 1'b1;
found = 1'b1;
end
end
end
// ── The decision ────────────────────────────────────────────────────────
// PRIORITY: a periodic obligation beats every opportunistic requester.
// This is section 3's rule and it is the one mutation in section 6 that
// breaks a guarantee rather than merely degrading service.
assign periodic_win = opportunity && periodic_due;
assign grant_valid = opportunity && !periodic_due && found;
assign grant = grant_valid ? sel : '0;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rr_ptr <= '0;
end else if (grant_valid) begin
// Resume AFTER the endpoint just served. Advancing past the grant --
// rather than to it -- is what makes the served endpoint last in line
// next time, which is the fairness property.
for (int unsigned i = 0; i < N_REQ; i++)
if (sel[i]) rr_ptr <= (i + 1) % N_REQ;
end
end
endmoduleWhat it models. One selection decision: periodic precedence, and round-robin among opportunistic requesters.
Engineering reason. Because the decision is where starvation is introduced, and starvation is a failure that produces no error anywhere.
Inputs. Clock and reset, a service opportunity, a periodic-due indication, and the pending requesters.
State retained. One pointer — $clog2(N_REQ) bits. For four requesters, two flip-flops.
Outputs. A one-hot grant with its validity, and an indication that the opportunity went to periodic traffic.
Hardware implied. A small ordered search — a rotated priority encoder in practice — plus the pointer register.
Reset behaviour. The pointer returns to zero. Nothing else is retained, so there is no stale-grant state to clear.
Assumptions. That req is stable during the cycle a grant is evaluated; that opportunity and periodic_due come from whatever owns the schedule; and that a granted requester is actually served, since the pointer advances on the grant.
Omissions. Everything in the header's list — and the list is long deliberately, because a block named arbiter invites being read as a scheduler.
What DV should verify. That a grant is one-hot or zero; that no grant occurs without a request; that no grant occurs while a periodic obligation is due; that a continuously-pending requester does not prevent others from being served; that every pending requester is eventually granted; and that the pointer advances past the granted requester rather than to it.
Round-robin over the residue — controller-domain view
8 cycles6. Mutation Test
Four mutations, run against the block in §5 and the checks in §7. The measurement bench applies 294 evaluated cycles across seven stimulus phases; the numbers below are what it reported.
The unmutated block first, so the mutant columns mean something:
| A1 one-hot | A2 grant≤req | A3 periodic | A4 opportunity | F1 fairness | ref model | |
|---|---|---|---|---|---|---|
| golden | 0 | 0 | 0 | 0 | 0 | 0 |
| B1 fixed priority | 0 | 0 | 0 | 0 | 54 | 94 |
| B2 bulk beats periodic | 0 | 0 | 41 | 0 | 0 | 72 |
| B3 pointer to, not past | 0 | 0 | 0 | 0 | 51 | 99 |
| B4 grant without opportunity | 0 | 0 | 0 | 44 | 0 | 72 |
The golden block also distributed exactly 10 grants to each of the four requesters over the 40-opportunity sustained-contention phase, which is the arithmetic round-robin promises.
B1 — fixed priority instead of round-robin
Remove the pointer and always search from zero.
Measured. Over the 40 opportunities with all four requesters pending continuously: EP0 = 40, EP1 = 0, EP2 = 0, EP3 = 0. Three requesters with data ready, never served once.
And every safety property passed — A1, A2, A3 and A4 all reported zero violations, exactly as the table shows. Every grant was one-hot, every grant had a request behind it, no grant occurred without an opportunity, and no periodic obligation was displaced.
F1 reported 54 violations. It is the only property that fired, and §7 is about why the natural ones cannot substitute for it.
B2 — let opportunistic traffic win over a periodic obligation
Invert the precedence.
Measured. A3 fired 41 times; F1 stayed clean at zero. That split is the interesting part: the arbiter remained perfectly fair and still broke a promise. Fairness among the bulk endpoints was never the issue — the reservation belonged to somebody who was not in the rotation at all.
This breaks a guarantee rather than degrading a service. The interrupt or isochronous endpoint was promised a visit, capacity was reserved for it, and the reservation is being spent on traffic that asked for nothing.
And the symptom is remote from the cause, which is what makes it expensive in the lab: the bulk device works perfectly — better than perfectly — and some other device's periodic endpoint misses its interval. On a bus with several devices the two may have nothing to do with each other.
B3 — advance the pointer to the granted requester rather than past it
if (sel[i]) rr_ptr <= i; // MUTANT B3: to, not pastMeasured. Over the same sustained-contention phase: EP0 = 40, EP1 = 0, EP2 = 0, EP3 = 0 — bit-for-bit the same distribution B1 produced. A1 through A4 clean; F1 fired 51 times.
The mechanism is that the search restarts at the requester just served, which is still pending, so it is immediately selected again. This is fixed priority with extra steps, and it produces exactly B1's starvation while appearing to implement round-robin.
The off-by-one is the entire fairness property, and it is invisible in code review unless you know to look: the line reads plausibly, the register is present, and a reviewer sees round-robin.
B4 — grant without an opportunity
Remove the opportunity term.
Measured. A4 fired 44 times; F1 stayed clean.
Grants are issued on cycles when no service is available, so a requester is marked served when nothing happened — and the pointer advances, moving that endpoint to the back of the queue for a transfer that never occurred.
A grant that does not correspond to service is worse than no grant, because the fairness mechanism is being driven by fictional events. F1's silence is the point: fairness over fictional grants is still fairness, and it is worthless.
7. The Assertions
// ─────────────────────────────────────────────────────────────────────────
// Classification: TEACHING ASSERTIONS about the arbiter.
// A-properties are SAFETY; F-properties are FAIRNESS, which is a bounded
// form of progress. Section 6 measured why safety alone is insufficient.
// ─────────────────────────────────────────────────────────────────────────
// A1 -- ONE-HOT. At most one requester is selected.
property p_grant_onehot;
@(posedge clk) disable iff (!rst_n)
grant_valid |-> $onehot(grant);
endproperty
assert property (p_grant_onehot);
// A2 -- NO GRANT WITHOUT A REQUEST. The granted requester must have been
// asking. Catches a selection that ignores `req` entirely.
property p_grant_implies_request;
@(posedge clk) disable iff (!rst_n)
grant_valid |-> ((grant & req) == grant);
endproperty
assert property (p_grant_implies_request);
// A3 -- PERIODIC PRECEDENCE. Section 3's rule, and the one whose violation
// breaks a promise made to a different endpoint (section 6's B2).
property p_periodic_wins;
@(posedge clk) disable iff (!rst_n)
(opportunity && periodic_due) |-> (!grant_valid && periodic_win);
endproperty
assert property (p_periodic_wins);
// A4 -- NO GRANT WITHOUT AN OPPORTUNITY. A grant must correspond to real
// service, or the fairness mechanism is driven by events that did not
// happen (section 6's B4).
property p_grant_implies_opportunity;
@(posedge clk) disable iff (!rst_n)
grant_valid |-> opportunity;
endproperty
assert property (p_grant_implies_opportunity);
// ── FAIRNESS ────────────────────────────────────────────────────────────
// F1 -- BOUNDED FAIRNESS. A requester that stays pending is granted within
// N_REQ opportunities. Bounded deliberately: an unbounded "eventually" is
// not checkable in simulation, and the bound is exactly what round-robin
// promises -- every other requester goes at most once before this one does.
//
// This is the ONLY property that catches section 6's B1 and B3, both of
// which satisfy every safety property above.
generate
for (genvar gi = 0; gi < N_REQ; gi++) begin : g_fair
// The genvar is elaborated into the property rather than passed as an
// argument: an index that is constant at elaboration keeps this within
// the subset every tool in the flow accepts.
property p_bounded_fairness;
@(posedge clk) disable iff (!rst_n)
(req[gi] && opportunity && !periodic_due)
|-> ##[0:N_REQ-1] (grant[gi] || !req[gi]);
endproperty
assert property (p_bounded_fairness);
end
endgenerateA1 to A4 are satisfied completely by a fixed-priority arbiter, which is §6's measurement stated as a property fact. They constrain what a grant means and say nothing about who gets one.
F1 is the fairness property, and three things about its form are deliberate:
- It is bounded. An unbounded eventually cannot fail in a finite simulation, so it would pass vacuously on B1 forever. The bound is
N_REQopportunities, which is exactly what round-robin promises. - Its antecedent is the request, not the grant. Chapter 9.5 §7 measured a progress property gated on the very signal whose failure it was meant to detect. Gating this on
grantwould make it vacuous under precisely B1. - It excuses a requester that withdraws.
|| !req[i]covers a requester that stops asking, which is not a fairness violation.
8. Verification
This chapter's commit point is the opportunity went to the right requester.
Stimulus. One requester pending; all requesters pending continuously — the case B1 and B3 need; requesters arriving and withdrawing; a periodic obligation coincident with pending requesters; an opportunity with nothing pending; and a reset with the pointer at each value.
The stimulus requirement §6 makes non-negotiable: all requesters pending simultaneously and continuously, for longer than N_REQ opportunities. Starvation is invisible with one requester, invisible with requesters that take turns naturally, and invisible in a short run. It requires sustained contention, which is the condition a functional test rarely creates and a real bus creates constantly.
Observation. Which requester was granted, across time. A single-cycle observation cannot see starvation at all — the defect is entirely in the distribution of grants over a window.
Reference model. A pointer and an ordered search, which is a five-line model. Its value is that it is written from the fairness rule rather than from the RTL, so an off-by-one in the pointer update — §6's B3 — shows up as a divergence rather than being reproduced.
And the measurement makes an argument for writing it. The reference model diverged on all four mutations — 94, 72, 99 and 72 mismatching cycles respectively — while no single property caught more than one of them. That is not an accident of this design:
- A property encodes one rule, and each of §6's mutants breaks a different one.
- A model written from the specification encodes the whole rule at once, so it diverges whenever the implementation departs from the specification in any direction, including directions nobody wrote a property for.
The corollary is the one worth keeping: properties tell you which rule broke, which is what you need while debugging; a reference model tells you that something broke, which is what you need to find out at all. Neither replaces the other, and a bench with only properties is only as complete as the list of rules somebody thought to write down.
Coverage — crosses:
- number of simultaneous requesters: 0, 1, 2, all
- periodic due × each number of pending requesters
- opportunity present × absent, with requests pending
- pointer value × which requesters are pending — all combinations for small
N_REQ - a requester withdrawing between the request and the grant
Negative cases with defined outcomes: no grant without an opportunity; no grant without a request; no grant while a periodic obligation is due; and no requester starved over a window of N_REQ opportunities.
9. Debugging: the Endpoint That Is Never Served
A device has two bulk IN endpoints. One works. The other has data continuously available and the host never collects any of it. No error is reported anywhere.
What does no error tell you? That nothing thinks it has failed. The endpoint has data, the host is not asking, and neither side considers that a fault — Chapter 9.4 §2 established that a device with data simply waits.
What is the first observation? Not the endpoint. The grants. Which requester is being selected, over a window long enough to see a pattern.
What does the pattern tell you? If the working endpoint is granted every opportunity, the arbitration is unfair and §6's B1 or B3 is the cause. If grants alternate correctly but the second endpoint's transfers fail, the problem is in the endpoint, not the arbiter.
How do you distinguish B1 from B3? By reading the pointer. B1 has none; B3 has one that does not advance past the granted requester. Both produce identical grant patterns, which is §6's point — the behaviour is the same and only the code differs.
What would a protocol analyser show? Traffic to one endpoint and none to the other. Correct traffic, correctly formed — the host is addressing what it was told to address. The absence is the evidence, and absences are hard to see in a trace full of activity.
And the signature to keep: an endpoint with data that is never addressed, with no error anywhere, is an arbitration failure — and it is diagnosed by looking at the distribution of service rather than at the endpoint that is not receiving it.
10. Common Misconceptions
11. Reason It Through
A device's bulk endpoint achieves its rated throughput on a bus by itself. Plugged into a hub alongside a webcam, throughput drops by more than half — and the drop is larger than the webcam's own data rate.
Why would the loss exceed the webcam's data rate? Because the webcam is not merely consuming bandwidth — it is reserving it. Chapter 10.1 §5: periodic reservations are held from configuration until un-configuration, used or not.
So what is the bulk device actually losing? The reservation, not the usage. If the webcam reserves capacity for its worst-case frame and typically sends less, the difference is unavailable to anyone — reserved for a flow that is not using it.
Is that a defect? No. It is the price of the guarantee, and Chapter 10.2 §3's chain is explicit: a guarantee to one party is a restriction on everybody else. The webcam's reservation is what makes its stream work.
What can the bulk device do about it? Nothing, and that is worth being clear about. It has no reservation to defend and no priority to raise. Its contract is the residue.
What can the system do? Move one device to a different bus, or — if the webcam is the engineer's own device — use Chapter 7.3 §3's alternate settings so it reserves nothing while idle. That is what alternate settings are for, and this is the scenario that makes them worth the complexity.
And the transferable point? Reserved capacity is consumed by the reservation, not by the traffic. A flow that reserves generously and sends sparsely hurts every non-reserved flow on the bus by the full reservation — which is why asking for a guarantee should be proportionate to what is actually needed rather than to the worst case anyone can imagine.
12. Understanding Check
13. Summary
Bulk is defined by its absences: no reservation, no service interval, no bounded latency. What it gets is correctness and retryability.
And the apparent contradiction resolves in one sentence: Bulk is promised no share of the bus and permitted all of it. Nothing reserves capacity for it and nothing caps it, so on a typical bus it takes nearly everything — which is why a storage device saturates a link while a reserved endpoint moves a few bytes per frame.
Throughput and guarantee are independent axes, and the stereotype Bulk is slow collapses them. A device needing high throughput should use Bulk because it has no guarantee: guarantees mean reservations, reservations are capped, and one large enough for bulk-scale data would consume the cap and be wasted whenever the device was idle.
The cost is variable, unbounded latency, which is harder than long latency for anything sizing a buffer downstream.
Sharing the residue is where the module's most instructive defect lives. §6 measured four mutations: fixed priority starves every lower requester permanently with every safety property passing; inverted precedence spends a reservation on traffic that asked for nothing, with the symptom appearing on a different device; advancing the pointer to the granted requester instead of past it produces identical starvation while keeping every structural sign of fairness — and is therefore invisible in review; and granting without an opportunity drives the fairness mechanism with events that did not happen.
Which forces the module's verification lesson: fairness is behavioural, not structural. It cannot be confirmed by reading code, and the only check that catches both starvation mutations is a bounded fairness property observing grants over a window — bounded because an unbounded eventually passes vacuously forever, and gated on the request rather than the grant because Chapter 9.5 measured what gating on the wrong signal costs.
14. What Comes Next
Bulk asked for nothing. The next type asks for something very specific, and the thing it asks for is not bandwidth.
Chapter 10.4 is Interrupt, and it begins by dismantling its own name: the device does not interrupt anything. USB remains host-driven, and what an interrupt endpoint actually buys is a promise about how often it will be asked — which is a guarantee about opportunity, not about data.
That distinction is the chapter, and it explains why a mouse producing a few bytes per second needs a reservation at all.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
Four Transfer Types Overview
The four transfer types are not a list to memorise — they fall out of two orthogonal questions plus one bootstrap question. Deriving them, and the policy block whose outputs are all derived and none stored.
- Related topic
Congestion Handling
What each layer should actually do once flow-control pressure exists — the four conditions that look alike, per-layer congestion responses, a hysteretic policy FSM with derived watermarks, congestion age and escalation, strict-priority starvation versus rotating-priority fairness, drain mode, admission throttling before ownership transfer, retry-induced congestion, congestion collapse, and bufferbloat.
- Related topic
Arbitration
Arbitration runs after legality has already decided. It narrows subsets rather than comparing priority numbers, advances its pointer only on commit, and needs a bounded bypass because row-hit-first genuinely starves.
- Related topic
Control Transfers
The type that exists to resolve the bootstrap problem: how a host manages a device it has not yet agreed anything with. Serialisation, the busy refusal, and a same-cycle ordering bug that made the contract wrong.
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.
