USB · Module 29
USB Webcams
UVC streams video over isochronous transfers, which have no retries. With only a frame-ID bit and an end-of-frame flag for framing, exactly 1 packet loss in P is detectable — 6% for a 16-packet frame, and under 1% for a real one.
The second case study, and the opposite shape from the first. A flash drive is a decision table with fatal cells. A webcam is a firehose with no error handling at all — because there is nothing to handle it with.
1. Isochronous Means Exactly One Thing
Bandwidth is reserved. Delivery is not.
BULK reserved bandwidth: none
delivery: guaranteed, by retrying until it works
ISOCHRONOUS reserved bandwidth: guaranteed, every frame
delivery: best effort, and that is the end of itThere is no ACK, no NAK, no retry and no retransmission. A packet that goes missing is gone, and nothing on the bus will ever mention it again. That is not a weakness in the mode — it is the point of it. A camera cannot use a retransmission that arrives after the frame has been displayed, so paying for one would be paying for nothing.
2. The Two Ways A Frame Can End
ended on EOF the normal way. It may still be SHORT -- a packet
was lost mid-frame -- and nothing says so.
ended on a FID toggle the EOF packet itself never arrived, so the next
frame's toggle is what closed this one. The
receiver CAN see this.Everything follows from that asymmetry:
| what was lost | what the receiver sees | can it tell? |
|---|---|---|
| a mid-frame packet with data | a frame that ended on EOF, short by those bytes | no |
| the EOF packet | a frame that ended on a FID toggle | yes |
| a zero-length packet | nothing at all — the stream is bit-identical | nothing to tell |
That last row is not a loophole. Zero-length isochronous packets are exactly how a device says "nothing this microframe" while keeping its bandwidth reservation alive, and a protocol in which losing one mattered would be a worse protocol.
3. The Design (Verilog-2005)
// =====================================================================
// uvc_frame_asm -- assembling video frames from a stream that has no
// retries, no acknowledgements and no sequence numbers.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a webcam. There is no sensor, no compression, no format
// negotiation and no still-image path. It is the part of a UVC receiver
// that decides where one frame ends and the next begins.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A webcam streams over ISOCHRONOUS transfers, and isochronous means
// exactly one thing: bandwidth is reserved and delivery is not.
// There is no ACK, no NAK, no retry and no CRC-driven retransmission.
// A packet that goes missing is simply gone, and nothing on the bus
// will ever mention it again.
//
// So the receiver has to work out the frame structure from what did
// arrive. UVC gives it exactly two bits to do that with, in the payload
// header on every packet:
//
// FID a frame-ID bit that TOGGLES between consecutive frames
// EOF set on the last packet of a frame
//
// That is the entire framing mechanism. There is no per-packet sequence
// number, no frame length field, and no byte counter to check against.
//
// THE CONSEQUENCE, WHICH IS THE POINT OF THIS CHAPTER
// ---------------------------------------------------
// With only those two bits, a lost packet is detectable ONLY if it
// happened to be the one carrying EOF:
//
// * lose the EOF packet, and the frame never terminates -- the next
// frame's FID toggle is what ends it, and the receiver can see that
// it ended the wrong way. DETECTABLE.
//
// * lose any other packet, and the frame still gets its EOF. It is
// short by however many bytes were in the lost packet, and there is
// nothing in the stream that says so. SILENT.
//
// One packet in P is the EOF packet, so the detectable fraction is 1/P.
// For a frame split into eight packets that is 12.5%; for a real
// 1080p frame, which is hundreds of packets, it is indistinguishable
// from zero.
//
// That is not a defect in this module and it is not a defect in UVC. It
// is what "no retries" costs, and it is why the error bit below exists
// and why real hosts throw away frames on the slightest suspicion.
// =====================================================================
module uvc_frame_asm (
input wire clk,
input wire rst_n,
// ---- one isochronous packet that ARRIVED ----
//
// Packets that were lost are simply never presented. The module has no
// way to know they existed, which is the whole situation being
// modelled.
input wire pkt_valid,
input wire pkt_fid, // bmHeaderInfo bit 0, toggles per frame
input wire pkt_eof, // bmHeaderInfo bit 1, last packet
input wire pkt_err, // bmHeaderInfo bit 6, device says this is bad
input wire [15:0] pkt_bytes, // payload bytes after the header
// ---- a completed frame ----
output wire frame_done,
output wire [31:0] frame_bytes,
output wire frame_fid,
// Did this frame end the way a frame is supposed to end?
//
// `frame_eof` distinguishes the two terminations, and it is the ONLY
// evidence of loss the receiver ever gets.
output wire frame_eof, // ended on EOF -- looked normal
output wire frame_flagged, // the device marked a packet bad
output wire [31:0] n_pkt,
output wire [31:0] n_frame,
output wire [31:0] n_truncated, // ended by a FID toggle, not by EOF
output wire [31:0] n_flagged,
output wire [31:0] n_toggle
);
reg started; // a frame is currently open
reg cur_fid; // the FID of the open frame
reg [31:0] acc; // bytes accumulated into the open frame
reg acc_err; // any packet in it carried the error bit
reg done_r, fid_r, eof_r, flag_r;
reg [31:0] bytes_r;
reg [31:0] pkt_c, frame_c, trunc_c, flag_c, tog_c;
assign frame_done = done_r;
assign frame_bytes = bytes_r;
assign frame_fid = fid_r;
assign frame_eof = eof_r;
assign frame_flagged = flag_r;
assign n_pkt = pkt_c;
assign n_frame = frame_c;
assign n_truncated = trunc_c;
assign n_flagged = flag_c;
assign n_toggle = tog_c;
// A packet whose FID differs from the open frame's means the previous
// frame ended without ever presenting EOF. That is the only loss signal
// this protocol has.
wire fid_toggled = started && (pkt_fid != cur_fid);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
started <= 1'b0;
cur_fid <= 1'b0;
acc <= 32'd0;
acc_err <= 1'b0;
done_r <= 1'b0;
fid_r <= 1'b0;
eof_r <= 1'b0;
flag_r <= 1'b0;
bytes_r <= 32'd0;
pkt_c <= 32'd0;
frame_c <= 32'd0;
trunc_c <= 32'd0;
flag_c <= 32'd0;
tog_c <= 32'd0;
end else begin
done_r <= 1'b0;
if (pkt_valid) begin
pkt_c <= pkt_c + 32'd1;
if (fid_toggled) begin
// ---- the previous frame ended the WRONG way ----
//
// It never showed EOF, so the packet carrying EOF did not
// arrive. Close it out and report it as truncated -- this is
// the one loss the receiver can actually see.
done_r <= 1'b1;
bytes_r <= acc;
fid_r <= cur_fid;
eof_r <= 1'b0;
flag_r <= acc_err;
frame_c <= frame_c + 32'd1;
trunc_c <= trunc_c + 32'd1;
tog_c <= tog_c + 32'd1;
// and the arriving packet opens the next one
started <= 1'b1;
cur_fid <= pkt_fid;
acc <= {16'd0, pkt_bytes};
acc_err <= pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
end else if (!started) begin
// ---- first packet of a new frame ----
started <= 1'b1;
cur_fid <= pkt_fid;
acc <= {16'd0, pkt_bytes};
acc_err <= pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
if (pkt_eof) begin
// a single-packet frame
done_r <= 1'b1;
bytes_r <= {16'd0, pkt_bytes};
fid_r <= pkt_fid;
eof_r <= 1'b1;
flag_r <= pkt_err;
frame_c <= frame_c + 32'd1;
started <= 1'b0;
end
end else begin
// ---- a continuation packet of the open frame ----
acc <= acc + {16'd0, pkt_bytes};
acc_err <= acc_err | pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
if (pkt_eof) begin
// ---- the frame ended the RIGHT way ----
//
// Note what is NOT checked here, because it cannot be: there
// is no length field and no sequence number, so a frame that
// is short by one lost packet arrives here looking exactly
// like a complete one.
done_r <= 1'b1;
bytes_r <= acc + {16'd0, pkt_bytes};
fid_r <= cur_fid;
eof_r <= 1'b1;
flag_r <= acc_err | pkt_err;
frame_c <= frame_c + 32'd1;
started <= 1'b0;
end
end
end
end
end
endmoduleTwo bits of framing, and the two things they can tell you
4. The Measurement
The bench knows what was sent. The design only knows what arrived. That asymmetry is the experiment: the bench generates a frame sequence, decides which packet to drop, presents only the survivors, and compares the design's view of each frame against the truth it is holding back.
The headline
Drop every droppable packet in turn, and count how many of those losses the receiver could have detected. Identical in Verilog, SystemVerilog and VHDL:
| packets per frame | losses tried | detectable | silent | detectable |
|---|---|---|---|---|
| 2 | 4 | 2 | 2 | 50% |
| 4 | 8 | 2 | 6 | 25% |
| 8 | 16 | 2 | 14 | 12% |
| 16 | 32 | 2 | 30 | 6% |
The detectable column is 2 in every row — one EOF packet per frame, two frames with a following frame to reveal them. Everything else scales, so the fraction is exactly 1/P.
Where the numbers come from
The directed sweep, identical across all three languages:
| steps | checks | reach | losses | detected | silent | errors | |
|---|---|---|---|---|---|---|---|
| directed only | 4,502 | 1,920 | 102 / 102 | 156 | 20 | 128 | 0 |
156 losses, 20 detected, 128 silent — and 8 that damaged nothing at all, because the packet thrown away was zero-length.
The exhaustive dimension set:
| dimension | values |
|---|---|
| which packet is lost | none, or any of 16 |
| device error flag | set, clear |
| payload profile | three sizes, including zero-length packets |
17 × 2 × 3 = 102 points, every one reachable.
The same loss, one packet apart, with completely different visibility
5. The Testbench (Verilog)
// =====================================================================
// Testbench for uvc_frame_asm.
//
// THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
//
// That asymmetry is the entire experiment. The bench generates a frame
// sequence, decides which packet to drop, presents only the survivors,
// and then compares the design's view of each frame against the truth
// it is holding back.
//
// Three outcomes are possible per frame, and telling them apart is the
// measurement:
//
// CLEAN every packet arrived; bytes match the truth
// SILENT a packet was lost mid-frame. The EOF still arrived, so
// the frame looks complete and is short. The DESIGN
// cannot tell. The bench can, because it knows the truth.
// DETECTED the EOF packet itself was lost, so the frame ended on
// the next frame's FID toggle instead. The design CAN
// tell.
//
// A packet is the EOF packet one time in P, so the detectable fraction
// is 1/P -- and the whole point of the headline sweep is that this is a
// property of the protocol, not of the implementation.
// =====================================================================
`timescale 1ns/1ps
module tb_uv_v;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg pkt_valid = 1'b0, pkt_fid = 1'b0, pkt_eof = 1'b0, pkt_err = 1'b0;
reg [15:0] pkt_bytes = 16'd0;
wire frame_done, frame_fid, frame_eof, frame_flagged;
wire [31:0] frame_bytes;
wire [31:0] n_pkt, n_frame, n_truncated, n_flagged, n_toggle;
uvc_frame_asm dut (
.clk(clk), .rst_n(rst_n),
.pkt_valid(pkt_valid), .pkt_fid(pkt_fid), .pkt_eof(pkt_eof),
.pkt_err(pkt_err), .pkt_bytes(pkt_bytes),
.frame_done(frame_done), .frame_bytes(frame_bytes),
.frame_fid(frame_fid), .frame_eof(frame_eof),
.frame_flagged(frame_flagged),
.n_pkt(n_pkt), .n_frame(n_frame), .n_truncated(n_truncated),
.n_flagged(n_flagged), .n_toggle(n_toggle)
);
integer errors = 0, checks = 0, steps = 0;
integer seed;
// $random is SIGNED: mask the sign bit before any modulo.
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
task ck(input cond, input [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step#%0d: %0s", $time, steps, what);
end
end
endtask
// ---- GROUND TRUTH, invisible to the design ----
//
// What each frame SHOULD have contained, and what actually reached the
// receiver. The difference between the two is the damage.
// Sized for the LONGEST sequence any phase runs, not for the shortest.
// These held 8 entries while phase 3 streams 16 frames and indexes
// got_eof[k] up to 15: Verilog returns x for that and says nothing,
// which is how it survived. VHDL makes the same read fatal.
integer true_bytes [0:31]; // bytes the sender put in frame f
integer sent_bytes [0:31]; // bytes that actually arrived for frame f
integer true_eof [0:31]; // did frame f's EOF packet arrive?
// ---- what the design reported, captured as it reports it ----
integer got_n;
integer got_bytes [0:31];
integer got_fid [0:31];
integer got_eof [0:31];
// Captured WITH the frame, not read afterwards. frame_flagged is only
// meaningful in the cycle frame_done is asserted; reading it later reads
// whatever the previous frame left behind.
integer got_flag [0:31];
// ---- the measurement ----
// m_* are PER-RUN flags: phase 2 zeroes them before each sequence so it
// can ask whether that particular loss was detectable. t_* are the
// cumulative totals, which nothing zeroes -- keeping them separate is
// what stops a directed-only run reporting zero losses.
integer m_clean = 0, m_silent = 0, m_detected = 0, m_losses = 0;
integer t_clean = 0, t_silent = 0, t_detected = 0, t_losses = 0;
// ---- cumulative across resets ----
//
// reset_dut runs once per sequence, so the DUT's own counters describe
// only the last one. Per-step checks still read the DUT counters; these
// are for the totals.
integer c_pkt = 0, c_frame = 0, c_trunc = 0, c_flag = 0;
// Per packets-per-frame, how many single-packet losses were detectable.
integer tab_p [0:4];
integer tab_total [0:4];
integer tab_det [0:4];
task reset_dut;
integer j;
begin
rst_n = 1'b0; pkt_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
got_n = 0;
for (j = 0; j < 32; j = j + 1) begin
true_bytes[j] = 0; sent_bytes[j] = 0; true_eof[j] = 0;
got_bytes[j] = 0; got_fid[j] = 0; got_eof[j] = 0; got_flag[j] = 0;
end
end
endtask
// Present one packet and collect any frame the design closes out.
task send_pkt(input fid, input eof, input err, input [15:0] bytes);
begin
pkt_valid = 1'b1; pkt_fid = fid; pkt_eof = eof;
pkt_err = err; pkt_bytes = bytes;
@(posedge clk); #1;
pkt_valid = 1'b0;
c_pkt = c_pkt + 1;
if (frame_done) begin
c_frame = c_frame + 1;
if (!frame_eof) c_trunc = c_trunc + 1;
if (frame_flagged) c_flag = c_flag + 1;
if (got_n < 32) begin
got_bytes[got_n] = frame_bytes;
got_fid[got_n] = frame_fid;
got_eof[got_n] = frame_eof;
got_flag[got_n] = frame_flagged;
end
got_n = got_n + 1;
end
steps = steps + 1;
end
endtask
// Let the design settle and collect a trailing frame, if any.
task drain;
integer g;
begin
for (g = 0; g < 4; g = g + 1) begin
@(posedge clk); #1;
if (frame_done) begin
c_frame = c_frame + 1;
if (!frame_eof) c_trunc = c_trunc + 1;
if (frame_flagged) c_flag = c_flag + 1;
if (got_n < 32) begin
got_bytes[got_n] = frame_bytes;
got_fid[got_n] = frame_fid;
got_eof[got_n] = frame_eof;
got_flag[got_n] = frame_flagged;
end
got_n = got_n + 1;
end
end
end
endtask
// -------------------------------------------------------------------
// Run three frames of P packets, dropping ONE packet if `lost` >= 0.
//
// Frames 0 and 1 are the ones a loss is injected into, so there is
// always a following frame whose FID toggle could reveal a missing
// EOF. Frame 2 always completes, which keeps the experiment about
// detectability rather than about the end of the stream.
// -------------------------------------------------------------------
task run_seq(input integer P, input integer lost, input integer base,
input errbit);
integer f, p, idx, sz;
integer exp_frames, e_det, e_silent;
integer lost_sz, lost_was_eof;
begin
reset_dut;
lost_sz = 0; lost_was_eof = 0;
for (f = 0; f < 3; f = f + 1) begin
true_bytes[f] = 0; sent_bytes[f] = 0; true_eof[f] = 0;
end
idx = 0;
for (f = 0; f < 3; f = f + 1) begin
for (p = 0; p < P; p = p + 1) begin
sz = base + ((p % 4) * 4);
true_bytes[f] = true_bytes[f] + sz;
if (idx != lost) begin
sent_bytes[f] = sent_bytes[f] + sz;
if (p == P - 1) true_eof[f] = 1;
send_pkt(f[0], (p == P - 1), errbit && (p == 0), sz[15:0]);
end else begin
// remember what was thrown away, so the damage can be
// predicted rather than merely bounded
lost_sz = sz;
lost_was_eof = (p == P - 1);
end
idx = idx + 1;
end
end
drain;
// ---- what SHOULD have been reported ----
//
// Derived from the truth arrays, which the design cannot see. Three
// frames are always emitted: frame 2 always completes, and frames 0
// and 1 each close either on their own EOF or on the following
// frame's FID toggle.
exp_frames = 3;
ck(got_n == exp_frames, "the wrong number of frames was reported");
e_det = 0; e_silent = 0;
for (f = 0; f < 3 && f < got_n; f = f + 1) begin
// ---- PROPERTY 1: the FID alternates, frame by frame ----
ck(got_fid[f] == (f % 2),
"a frame was reported with the wrong frame ID");
// ---- PROPERTY 2: a frame ends on EOF exactly when its EOF arrived ----
//
// This is the design's ONLY loss signal, so it has to be exact.
ck(got_eof[f] == true_eof[f],
"frame_eof does not say whether the EOF packet arrived");
// ---- PROPERTY 3: the byte count is what ARRIVED ----
//
// Not what was sent. The design is not wrong to report fewer
// bytes; it is reporting what it received, which is all it has.
ck(got_bytes[f] == sent_bytes[f],
"the frame byte count does not match the bytes that arrived");
// ---- THE MEASUREMENT ----
//
// Classify the damage from the TRUTH, then record whether the
// design could have known.
if (sent_bytes[f] != true_bytes[f] || true_eof[f] == 0) begin
if (got_eof[f] == 0) e_det = e_det + 1; // ended wrong: visible
else e_silent = e_silent + 1; // looked fine: invisible
end
end
if (lost >= 0) begin
m_losses = m_losses + 1;
// ---- PROPERTY 4: the damage is exactly predictable ----
//
// Not merely "at most one frame". Which of the three outcomes
// occurs is decided entirely by two facts the bench knows about
// the packet it threw away:
//
// it carried EOF -> the frame ends on the next FID
// toggle instead. DETECTED.
// it carried bytes -> the frame still gets its EOF and
// is silently short. SILENT.
// it carried NEITHER -> a zero-length packet that was not
// the last one. Losing it costs
// nothing at all, and the stream is
// bit-identical to one where it was
// never sent.
//
// That last case is not a loophole. Zero-length isochronous
// packets are exactly how a device says "nothing this microframe"
// while keeping its bandwidth reservation alive, and a protocol
// in which losing one mattered would be a worse protocol.
//
// The first version of this check asserted "exactly one frame
// damaged" and fired 30 times against a correct design, all of
// them zero-byte losses.
if (lost_was_eof) begin
ck(e_det == 1 && e_silent == 0,
"losing the EOF packet was not reported as a truncated frame");
end else if (lost_sz > 0) begin
ck(e_silent == 1 && e_det == 0,
"losing a mid-frame packet was not a silently short frame");
end else begin
ck(e_det + e_silent == 0,
"losing a zero-length packet damaged a frame");
end
if (e_det > 0) begin m_detected = m_detected + 1; t_detected = t_detected + 1; end
if (e_silent > 0) begin m_silent = m_silent + 1; t_silent = t_silent + 1; end
t_losses = t_losses + 1;
end else begin
// ---- PROPERTY 5: with nothing lost, nothing is damaged ----
//
// The false-positive half. A detector that flags loss on a clean
// stream gets switched off, and then detects nothing at all.
ck(e_det + e_silent == 0,
"a frame was reported damaged on a stream with no loss");
m_clean = m_clean + 1; t_clean = t_clean + 1;
end
end
endtask
// ---- exhaustive reach ----
//
// loss position (none + 16) x error bit (2) x payload base (3) = 102,
// at P = 8. Every dimension is an independent input.
reg reach [0:101];
integer nr, ri;
integer li, eb, bs, pi, k, P, idx2;
integer PVAL [0:3];
initial begin
for (ri = 0; ri < 102; ri = ri + 1) reach[ri] = 1'b0;
for (k = 0; k < 5; k = k + 1) begin
tab_p[k] = 0; tab_total[k] = 0; tab_det[k] = 0;
end
PVAL[0] = 2; PVAL[1] = 4; PVAL[2] = 8; PVAL[3] = 16;
seed = 32'd29002;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
//
// P = 8, three frames, so sixteen droppable positions plus the
// no-loss case, crossed with the error bit and three payload
// profiles. 17 x 2 x 3 = 102.
// =============================================================
for (li = -1; li < 16; li = li + 1)
for (eb = 0; eb < 2; eb = eb + 1)
for (bs = 0; bs < 3; bs = bs + 1) begin
run_seq(8, li, bs * 8, eb[0]);
ri = ((li + 1) * 2 + eb) * 3 + bs;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
//
// For each packets-per-frame, drop every droppable packet in turn
// and count how many of those losses the receiver could detect.
//
// This is a property of the PROTOCOL: exactly one packet per frame
// carries EOF, so exactly one loss per frame is visible. The sweep
// measures it rather than asserting it.
// =============================================================
for (pi = 0; pi < 4; pi = pi + 1) begin
P = PVAL[pi];
tab_p[pi] = P;
for (li = 0; li < 2 * P; li = li + 1) begin
m_detected = 0; m_silent = 0;
run_seq(P, li, 8, 1'b0);
tab_total[pi] = tab_total[pi] + 1;
if (m_detected > 0) tab_det[pi] = tab_det[pi] + 1;
end
end
m_detected = 0; m_silent = 0; m_losses = 0; m_clean = 0;
// =============================================================
// PHASE 3 (DIRECTED) -- a long clean stream.
//
// Sixteen frames, nothing lost. Every frame must end on EOF with
// the full byte count and an alternating FID. This is the
// false-positive test: a loss detector that fires here is worse
// than none.
// =============================================================
reset_dut;
for (k = 0; k < 16; k = k + 1) begin : clean
integer p2, tot;
tot = 0;
for (p2 = 0; p2 < 4; p2 = p2 + 1) begin
tot = tot + 8 + p2;
send_pkt(k[0], (p2 == 3), 1'b0, (8 + p2));
end
drain;
ck(got_n == k + 1, "a clean stream lost a frame");
if (got_n == k + 1) begin
ck(got_eof[k] == 1, "a clean frame did not end on EOF");
ck(got_bytes[k] == tot, "a clean frame reported the wrong byte count");
ck(got_fid[k] == (k % 2), "a clean frame had the wrong frame ID");
end
end
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
//
// The one loss signal that does NOT depend on the framing bits: a
// device that knows it dropped data can say so. Swept over which
// packet of the frame carries it, because a flag on the last packet
// and a flag on the first must both survive to the frame report.
// =============================================================
for (pi = 0; pi < 3; pi = pi + 1)
for (k = -1; k < PVAL[pi]; k = k + 1) begin : errsweep
integer p3;
reset_dut;
P = PVAL[pi];
for (p3 = 0; p3 < P; p3 = p3 + 1)
send_pkt(1'b0, (p3 == P - 1), (p3 == k), 16'd8);
drain;
ck(got_n == 1, "the flagged frame was not reported");
if (got_n == 1) begin
// ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
//
// The device sets it on whichever packet it knows is damaged --
// the first, the last, or one in the middle. A receiver that only
// honours it on the first packet works on the case people
// demonstrate and fails on the ones that happen.
//
// k = -1 is the no-flag control. Without it this property would be
// satisfied by a receiver that flagged every frame.
ck(got_flag[0] == ((k >= 0) ? 1 : 0),
"the frame error flag does not reflect the packet that carried it");
end
end
// =============================================================
// PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
//
// A frame whose first packet also carries EOF takes a branch nothing
// else reaches. It is not a corner case invented for coverage: a
// still-image capture arrives on the same endpoint as the video
// stream and is routinely a single payload, and a small enough video
// frame is one too.
//
// Without this phase the branch was reached only when a packet loss
// happened to reduce a two-packet frame to one, which gave the
// mutation that breaks it a domain of four.
// =============================================================
reset_dut;
for (k = 0; k < 8; k = k + 1) begin : singles
send_pkt(k[0], 1'b1, (k % 3 == 0), 16'd64);
ck(got_n == k + 1, "a single-packet frame was not closed out");
if (got_n == k + 1) begin
ck(got_eof[k] == 1, "a single-packet frame did not end on EOF");
ck(got_bytes[k] == 64, "a single-packet frame reported the wrong size");
ck(got_fid[k] == (k % 2), "a single-packet frame had the wrong frame ID");
ck(got_flag[k] == ((k % 3 == 0) ? 1 : 0),
"a single-packet frame lost its error flag");
end
end
// =============================================================
// PHASE 5 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
for (k = 0; k < 400; k = k + 1) begin
P = 2 + (urand(0) % 7);
li = (urand(0) % 3 == 0) ? -1 : (urand(0) % (2 * P));
run_seq(P, li, (urand(0) % 3) * 8, (urand(0) % 5) == 0);
end
`endif
nr = 0; for (ri = 0; ri < 102; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/102 errors=%0d",
steps, checks, nr, errors);
$display("[uvc] packets=%0d frames=%0d truncated=%0d flagged_frames=%0d",
c_pkt, c_frame, c_trunc, c_flag);
$display("[uvc] losses injected=%0d detected=%0d SILENT=%0d clean runs=%0d",
t_losses, t_detected, t_silent, t_clean);
$display("--- what fraction of a single packet loss is even visible? ---");
$display(" packets/frame losses detectable silent detectable %%");
for (k = 0; k < 4; k = k + 1)
$display(" %13d %8d %12d %8d %11d",
tab_p[k], tab_total[k], tab_det[k], tab_total[k] - tab_det[k],
(tab_det[k] * 100) / tab_total[k]);
if (nr != 102) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmodule6. SystemVerilog
// =====================================================================
// uvc_frame_asm -- SystemVerilog.
//
// Same hardware contract as the Verilog file: same ports, same widths,
// same reset values, same cycle-by-cycle behaviour. `always_ff` replaces
// `always @(posedge ...)`, and the one continuous assignment is written
// as `logic` + `assign` on separate lines rather than as an initialised
// declaration -- `logic x = expr;` is a one-shot variable initialiser in
// SystemVerilog, not a continuous assignment.
//
// uvc_frame_asm -- assembling video frames from a stream that has no
// retries, no acknowledgements and no sequence numbers.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a webcam. There is no sensor, no compression, no format
// negotiation and no still-image path. It is the part of a UVC receiver
// that decides where one frame ends and the next begins.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A webcam streams over ISOCHRONOUS transfers, and isochronous means
// exactly one thing: bandwidth is reserved and delivery is not.
// There is no ACK, no NAK, no retry and no CRC-driven retransmission.
// A packet that goes missing is simply gone, and nothing on the bus
// will ever mention it again.
//
// So the receiver has to work out the frame structure from what did
// arrive. UVC gives it exactly two bits to do that with, in the payload
// header on every packet:
//
// FID a frame-ID bit that TOGGLES between consecutive frames
// EOF set on the last packet of a frame
//
// That is the entire framing mechanism. There is no per-packet sequence
// number, no frame length field, and no byte counter to check against.
//
// THE CONSEQUENCE, WHICH IS THE POINT OF THIS CHAPTER
// ---------------------------------------------------
// With only those two bits, a lost packet is detectable ONLY if it
// happened to be the one carrying EOF:
//
// * lose the EOF packet, and the frame never terminates -- the next
// frame's FID toggle is what ends it, and the receiver can see that
// it ended the wrong way. DETECTABLE.
//
// * lose any other packet, and the frame still gets its EOF. It is
// short by however many bytes were in the lost packet, and there is
// nothing in the stream that says so. SILENT.
//
// One packet in P is the EOF packet, so the detectable fraction is 1/P.
// For a frame split into eight packets that is 12.5%; for a real
// 1080p frame, which is hundreds of packets, it is indistinguishable
// from zero.
//
// That is not a defect in this module and it is not a defect in UVC. It
// is what "no retries" costs, and it is why the error bit below exists
// and why real hosts throw away frames on the slightest suspicion.
// =====================================================================
module uvc_frame_asm (
input logic clk,
input logic rst_n,
// ---- one isochronous packet that ARRIVED ----
//
// Packets that were lost are simply never presented. The module has no
// way to know they existed, which is the whole situation being
// modelled.
input logic pkt_valid,
input logic pkt_fid, // bmHeaderInfo bit 0, toggles per frame
input logic pkt_eof, // bmHeaderInfo bit 1, last packet
input logic pkt_err, // bmHeaderInfo bit 6, device says this is bad
input logic [15:0] pkt_bytes, // payload bytes after the header
// ---- a completed frame ----
output logic frame_done,
output logic [31:0] frame_bytes,
output logic frame_fid,
// Did this frame end the way a frame is supposed to end?
//
// `frame_eof` distinguishes the two terminations, and it is the ONLY
// evidence of loss the receiver ever gets.
output logic frame_eof, // ended on EOF -- looked normal
output logic frame_flagged, // the device marked a packet bad
output logic [31:0] n_pkt,
output logic [31:0] n_frame,
output logic [31:0] n_truncated, // ended by a FID toggle, not by EOF
output logic [31:0] n_flagged,
output logic [31:0] n_toggle
);
logic started; // a frame is currently open
logic cur_fid; // the FID of the open frame
logic [31:0] acc; // bytes accumulated into the open frame
logic acc_err; // any packet in it carried the error bit
logic done_r, fid_r, eof_r, flag_r;
logic [31:0] bytes_r;
logic [31:0] pkt_c, frame_c, trunc_c, flag_c, tog_c;
assign frame_done = done_r;
assign frame_bytes = bytes_r;
assign frame_fid = fid_r;
assign frame_eof = eof_r;
assign frame_flagged = flag_r;
assign n_pkt = pkt_c;
assign n_frame = frame_c;
assign n_truncated = trunc_c;
assign n_flagged = flag_c;
assign n_toggle = tog_c;
// A packet whose FID differs from the open frame's means the previous
// frame ended without ever presenting EOF. That is the only loss signal
// this protocol has.
logic fid_toggled;
assign fid_toggled = started && (pkt_fid != cur_fid);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
started <= 1'b0;
cur_fid <= 1'b0;
acc <= 32'd0;
acc_err <= 1'b0;
done_r <= 1'b0;
fid_r <= 1'b0;
eof_r <= 1'b0;
flag_r <= 1'b0;
bytes_r <= 32'd0;
pkt_c <= 32'd0;
frame_c <= 32'd0;
trunc_c <= 32'd0;
flag_c <= 32'd0;
tog_c <= 32'd0;
end else begin
done_r <= 1'b0;
if (pkt_valid) begin
pkt_c <= pkt_c + 32'd1;
if (fid_toggled) begin
// ---- the previous frame ended the WRONG way ----
//
// It never showed EOF, so the packet carrying EOF did not
// arrive. Close it out and report it as truncated -- this is
// the one loss the receiver can actually see.
done_r <= 1'b1;
bytes_r <= acc;
fid_r <= cur_fid;
eof_r <= 1'b0;
flag_r <= acc_err;
frame_c <= frame_c + 32'd1;
trunc_c <= trunc_c + 32'd1;
tog_c <= tog_c + 32'd1;
// and the arriving packet opens the next one
started <= 1'b1;
cur_fid <= pkt_fid;
acc <= {16'd0, pkt_bytes};
acc_err <= pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
end else if (!started) begin
// ---- first packet of a new frame ----
started <= 1'b1;
cur_fid <= pkt_fid;
acc <= {16'd0, pkt_bytes};
acc_err <= pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
if (pkt_eof) begin
// a single-packet frame
done_r <= 1'b1;
bytes_r <= {16'd0, pkt_bytes};
fid_r <= pkt_fid;
eof_r <= 1'b1;
flag_r <= pkt_err;
frame_c <= frame_c + 32'd1;
started <= 1'b0;
end
end else begin
// ---- a continuation packet of the open frame ----
acc <= acc + {16'd0, pkt_bytes};
acc_err <= acc_err | pkt_err;
if (pkt_err) flag_c <= flag_c + 32'd1;
if (pkt_eof) begin
// ---- the frame ended the RIGHT way ----
//
// Note what is NOT checked here, because it cannot be: there
// is no length field and no sequence number, so a frame that
// is short by one lost packet arrives here looking exactly
// like a complete one.
done_r <= 1'b1;
bytes_r <= acc + {16'd0, pkt_bytes};
fid_r <= cur_fid;
eof_r <= 1'b1;
flag_r <= acc_err | pkt_err;
frame_c <= frame_c + 32'd1;
started <= 1'b0;
end
end
end
end
end
endmoduleThe SystemVerilog testbench
// =====================================================================
// Testbench for uvc_frame_asm. -- SystemVerilog.
//
// SAME SEED AND SAME PHASE ORDER AS THE VERILOG BENCH, deliberately.
// Icarus seeds $random identically, so both drive identical stimulus and
// any difference between the two mutation columns is a real difference
// between the two DESIGNS. The independent-stimulus role is VHDL's.
//
// THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
//
// That asymmetry is the entire experiment. The bench generates a frame
// sequence, decides which packet to drop, presents only the survivors,
// and then compares the design's view of each frame against the truth
// it is holding back.
//
// Three outcomes are possible per frame, and telling them apart is the
// measurement:
//
// CLEAN every packet arrived; bytes match the truth
// SILENT a packet was lost mid-frame. The EOF still arrived, so
// the frame looks complete and is short. The DESIGN
// cannot tell. The bench can, because it knows the truth.
// DETECTED the EOF packet itself was lost, so the frame ended on
// the next frame's FID toggle instead. The design CAN
// tell.
//
// A packet is the EOF packet one time in P, so the detectable fraction
// is 1/P -- and the whole point of the headline sweep is that this is a
// property of the protocol, not of the implementation.
// =====================================================================
`timescale 1ns/1ps
module tb_uv_sv;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
logic pkt_valid = 1'b0, pkt_fid = 1'b0, pkt_eof = 1'b0, pkt_err = 1'b0;
logic [15:0] pkt_bytes = 16'd0;
logic frame_done, frame_fid, frame_eof, frame_flagged;
logic [31:0] frame_bytes;
logic [31:0] n_pkt, n_frame, n_truncated, n_flagged, n_toggle;
uvc_frame_asm dut (
.clk(clk), .rst_n(rst_n),
.pkt_valid(pkt_valid), .pkt_fid(pkt_fid), .pkt_eof(pkt_eof),
.pkt_err(pkt_err), .pkt_bytes(pkt_bytes),
.frame_done(frame_done), .frame_bytes(frame_bytes),
.frame_fid(frame_fid), .frame_eof(frame_eof),
.frame_flagged(frame_flagged),
.n_pkt(n_pkt), .n_frame(n_frame), .n_truncated(n_truncated),
.n_flagged(n_flagged), .n_toggle(n_toggle)
);
int errors = 0, checks = 0, steps = 0;
int seed;
// $random is SIGNED: mask the sign bit before any modulo.
function automatic logic [31:0] urand();
return $random(seed) & 32'h3FFF_FFFF;
endfunction
task automatic ck(input logic cond, input string what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step#%0d: %s", $time, steps, what);
end
end
endtask
// ---- GROUND TRUTH, invisible to the design ----
//
// What each frame SHOULD have contained, and what actually reached the
// receiver. The difference between the two is the damage.
// Sized for the LONGEST sequence any phase runs, not for the shortest.
// These held 8 entries while phase 3 streams 16 frames and indexes
// got_eof[k] up to 15: Verilog returns x for that and says nothing,
// which is how it survived. VHDL makes the same read fatal.
int true_bytes [0:31]; // bytes the sender put in frame f
int sent_bytes [0:31]; // bytes that actually arrived for frame f
int true_eof [0:31]; // did frame f's EOF packet arrive?
// ---- what the design reported, captured as it reports it ----
int got_n;
int got_bytes [0:31];
int got_fid [0:31];
int got_eof [0:31];
// Captured WITH the frame, not read afterwards. frame_flagged is only
// meaningful in the cycle frame_done is asserted; reading it later reads
// whatever the previous frame left behind.
int got_flag [0:31];
// ---- the measurement ----
// m_* are PER-RUN flags: phase 2 zeroes them before each sequence so it
// can ask whether that particular loss was detectable. t_* are the
// cumulative totals, which nothing zeroes -- keeping them separate is
// what stops a directed-only run reporting zero losses.
int m_clean = 0, m_silent = 0, m_detected = 0, m_losses = 0;
int t_clean = 0, t_silent = 0, t_detected = 0, t_losses = 0;
// ---- cumulative across resets ----
//
// reset_dut runs once per sequence, so the DUT's own counters describe
// only the last one. Per-step checks still read the DUT counters; these
// are for the totals.
int c_pkt = 0, c_frame = 0, c_trunc = 0, c_flag = 0;
// Per packets-per-frame, how many single-packet losses were detectable.
int tab_p [0:4];
int tab_total [0:4];
int tab_det [0:4];
task automatic reset_dut;
int j;
begin
rst_n = 1'b0; pkt_valid = 1'b0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
got_n = 0;
for (j = 0; j < 32; j = j + 1) begin
true_bytes[j] = 0; sent_bytes[j] = 0; true_eof[j] = 0;
got_bytes[j] = 0; got_fid[j] = 0; got_eof[j] = 0; got_flag[j] = 0;
end
end
endtask
// Present one packet and collect any frame the design closes out.
task automatic send_pkt(input fid, input eof, input err, input [15:0] bytes);
begin
pkt_valid = 1'b1; pkt_fid = fid; pkt_eof = eof;
pkt_err = err; pkt_bytes = bytes;
@(posedge clk); #1;
pkt_valid = 1'b0;
c_pkt = c_pkt + 1;
if (frame_done) begin
c_frame = c_frame + 1;
if (!frame_eof) c_trunc = c_trunc + 1;
if (frame_flagged) c_flag = c_flag + 1;
if (got_n < 32) begin
got_bytes[got_n] = frame_bytes;
got_fid[got_n] = frame_fid;
got_eof[got_n] = frame_eof;
got_flag[got_n] = frame_flagged;
end
got_n = got_n + 1;
end
steps = steps + 1;
end
endtask
// Let the design settle and collect a trailing frame, if any.
task automatic drain;
int g;
begin
for (g = 0; g < 4; g = g + 1) begin
@(posedge clk); #1;
if (frame_done) begin
c_frame = c_frame + 1;
if (!frame_eof) c_trunc = c_trunc + 1;
if (frame_flagged) c_flag = c_flag + 1;
if (got_n < 32) begin
got_bytes[got_n] = frame_bytes;
got_fid[got_n] = frame_fid;
got_eof[got_n] = frame_eof;
got_flag[got_n] = frame_flagged;
end
got_n = got_n + 1;
end
end
end
endtask
// -------------------------------------------------------------------
// Run three frames of P packets, dropping ONE packet if `lost` >= 0.
//
// Frames 0 and 1 are the ones a loss is injected into, so there is
// always a following frame whose FID toggle could reveal a missing
// EOF. Frame 2 always completes, which keeps the experiment about
// detectability rather than about the end of the stream.
// -------------------------------------------------------------------
task automatic run_seq(input integer P, input integer lost, input integer base,
input errbit);
int f, p, idx, sz;
int exp_frames, e_det, e_silent;
int lost_sz, lost_was_eof;
begin
reset_dut;
lost_sz = 0; lost_was_eof = 0;
for (f = 0; f < 3; f = f + 1) begin
true_bytes[f] = 0; sent_bytes[f] = 0; true_eof[f] = 0;
end
idx = 0;
for (f = 0; f < 3; f = f + 1) begin
for (p = 0; p < P; p = p + 1) begin
sz = base + ((p % 4) * 4);
true_bytes[f] = true_bytes[f] + sz;
if (idx != lost) begin
sent_bytes[f] = sent_bytes[f] + sz;
if (p == P - 1) true_eof[f] = 1;
send_pkt(f[0], (p == P - 1), errbit && (p == 0), sz[15:0]);
end else begin
// remember what was thrown away, so the damage can be
// predicted rather than merely bounded
lost_sz = sz;
lost_was_eof = (p == P - 1);
end
idx = idx + 1;
end
end
drain;
// ---- what SHOULD have been reported ----
//
// Derived from the truth arrays, which the design cannot see. Three
// frames are always emitted: frame 2 always completes, and frames 0
// and 1 each close either on their own EOF or on the following
// frame's FID toggle.
exp_frames = 3;
ck(got_n == exp_frames, "the wrong number of frames was reported");
e_det = 0; e_silent = 0;
for (f = 0; f < 3 && f < got_n; f = f + 1) begin
// ---- PROPERTY 1: the FID alternates, frame by frame ----
ck(got_fid[f] == (f % 2),
"a frame was reported with the wrong frame ID");
// ---- PROPERTY 2: a frame ends on EOF exactly when its EOF arrived ----
//
// This is the design's ONLY loss signal, so it has to be exact.
ck(got_eof[f] == true_eof[f],
"frame_eof does not say whether the EOF packet arrived");
// ---- PROPERTY 3: the byte count is what ARRIVED ----
//
// Not what was sent. The design is not wrong to report fewer
// bytes; it is reporting what it received, which is all it has.
ck(got_bytes[f] == sent_bytes[f],
"the frame byte count does not match the bytes that arrived");
// ---- THE MEASUREMENT ----
//
// Classify the damage from the TRUTH, then record whether the
// design could have known.
if (sent_bytes[f] != true_bytes[f] || true_eof[f] == 0) begin
if (got_eof[f] == 0) e_det = e_det + 1; // ended wrong: visible
else e_silent = e_silent + 1; // looked fine: invisible
end
end
if (lost >= 0) begin
m_losses = m_losses + 1;
// ---- PROPERTY 4: the damage is exactly predictable ----
//
// Not merely "at most one frame". Which of the three outcomes
// occurs is decided entirely by two facts the bench knows about
// the packet it threw away:
//
// it carried EOF -> the frame ends on the next FID
// toggle instead. DETECTED.
// it carried bytes -> the frame still gets its EOF and
// is silently short. SILENT.
// it carried NEITHER -> a zero-length packet that was not
// the last one. Losing it costs
// nothing at all, and the stream is
// bit-identical to one where it was
// never sent.
//
// That last case is not a loophole. Zero-length isochronous
// packets are exactly how a device says "nothing this microframe"
// while keeping its bandwidth reservation alive, and a protocol
// in which losing one mattered would be a worse protocol.
//
// The first version of this check asserted "exactly one frame
// damaged" and fired 30 times against a correct design, all of
// them zero-byte losses.
if (lost_was_eof) begin
ck(e_det == 1 && e_silent == 0,
"losing the EOF packet was not reported as a truncated frame");
end else if (lost_sz > 0) begin
ck(e_silent == 1 && e_det == 0,
"losing a mid-frame packet was not a silently short frame");
end else begin
ck(e_det + e_silent == 0,
"losing a zero-length packet damaged a frame");
end
if (e_det > 0) begin m_detected = m_detected + 1; t_detected = t_detected + 1; end
if (e_silent > 0) begin m_silent = m_silent + 1; t_silent = t_silent + 1; end
t_losses = t_losses + 1;
end else begin
// ---- PROPERTY 5: with nothing lost, nothing is damaged ----
//
// The false-positive half. A detector that flags loss on a clean
// stream gets switched off, and then detects nothing at all.
ck(e_det + e_silent == 0,
"a frame was reported damaged on a stream with no loss");
m_clean = m_clean + 1; t_clean = t_clean + 1;
end
end
endtask
// ---- exhaustive reach ----
//
// loss position (none + 16) x error bit (2) x payload base (3) = 102,
// at P = 8. Every dimension is an independent input.
logic reach [0:101];
int nr, ri;
int li, eb, bs, pi, k, P, idx2;
int PVAL [0:3];
initial begin
for (ri = 0; ri < 102; ri = ri + 1) reach[ri] = 1'b0;
for (k = 0; k < 5; k = k + 1) begin
tab_p[k] = 0; tab_total[k] = 0; tab_det[k] = 0;
end
PVAL[0] = 2; PVAL[1] = 4; PVAL[2] = 8; PVAL[3] = 16;
seed = 32'd29002;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
//
// P = 8, three frames, so sixteen droppable positions plus the
// no-loss case, crossed with the error bit and three payload
// profiles. 17 x 2 x 3 = 102.
// =============================================================
for (li = -1; li < 16; li = li + 1)
for (eb = 0; eb < 2; eb = eb + 1)
for (bs = 0; bs < 3; bs = bs + 1) begin
run_seq(8, li, bs * 8, eb[0]);
ri = ((li + 1) * 2 + eb) * 3 + bs;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
//
// For each packets-per-frame, drop every droppable packet in turn
// and count how many of those losses the receiver could detect.
//
// This is a property of the PROTOCOL: exactly one packet per frame
// carries EOF, so exactly one loss per frame is visible. The sweep
// measures it rather than asserting it.
// =============================================================
for (pi = 0; pi < 4; pi = pi + 1) begin
P = PVAL[pi];
tab_p[pi] = P;
for (li = 0; li < 2 * P; li = li + 1) begin
m_detected = 0; m_silent = 0;
run_seq(P, li, 8, 1'b0);
tab_total[pi] = tab_total[pi] + 1;
if (m_detected > 0) tab_det[pi] = tab_det[pi] + 1;
end
end
m_detected = 0; m_silent = 0; m_losses = 0; m_clean = 0;
// =============================================================
// PHASE 3 (DIRECTED) -- a long clean stream.
//
// Sixteen frames, nothing lost. Every frame must end on EOF with
// the full byte count and an alternating FID. This is the
// false-positive test: a loss detector that fires here is worse
// than none.
// =============================================================
reset_dut;
for (k = 0; k < 16; k = k + 1) begin : clean
int p2, tot;
tot = 0;
for (p2 = 0; p2 < 4; p2 = p2 + 1) begin
tot = tot + 8 + p2;
send_pkt(k[0], (p2 == 3), 1'b0, (8 + p2));
end
drain;
ck(got_n == k + 1, "a clean stream lost a frame");
if (got_n == k + 1) begin
ck(got_eof[k] == 1, "a clean frame did not end on EOF");
ck(got_bytes[k] == tot, "a clean frame reported the wrong byte count");
ck(got_fid[k] == (k % 2), "a clean frame had the wrong frame ID");
end
end
// =============================================================
// PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
//
// The one loss signal that does NOT depend on the framing bits: a
// device that knows it dropped data can say so. Swept over which
// packet of the frame carries it, because a flag on the last packet
// and a flag on the first must both survive to the frame report.
// =============================================================
for (pi = 0; pi < 3; pi = pi + 1)
for (k = -1; k < PVAL[pi]; k = k + 1) begin : errsweep
int p3;
reset_dut;
P = PVAL[pi];
for (p3 = 0; p3 < P; p3 = p3 + 1)
send_pkt(1'b0, (p3 == P - 1), (p3 == k), 16'd8);
drain;
ck(got_n == 1, "the flagged frame was not reported");
if (got_n == 1) begin
// ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
//
// The device sets it on whichever packet it knows is damaged --
// the first, the last, or one in the middle. A receiver that only
// honours it on the first packet works on the case people
// demonstrate and fails on the ones that happen.
//
// k = -1 is the no-flag control. Without it this property would be
// satisfied by a receiver that flagged every frame.
ck(got_flag[0] == ((k >= 0) ? 1 : 0),
"the frame error flag does not reflect the packet that carried it");
end
end
// =============================================================
// PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
//
// A frame whose first packet also carries EOF takes a branch nothing
// else reaches. It is not a corner case invented for coverage: a
// still-image capture arrives on the same endpoint as the video
// stream and is routinely a single payload, and a small enough video
// frame is one too.
//
// Without this phase the branch was reached only when a packet loss
// happened to reduce a two-packet frame to one, which gave the
// mutation that breaks it a domain of four.
// =============================================================
reset_dut;
for (k = 0; k < 8; k = k + 1) begin : singles
send_pkt(k[0], 1'b1, (k % 3 == 0), 16'd64);
ck(got_n == k + 1, "a single-packet frame was not closed out");
if (got_n == k + 1) begin
ck(got_eof[k] == 1, "a single-packet frame did not end on EOF");
ck(got_bytes[k] == 64, "a single-packet frame reported the wrong size");
ck(got_fid[k] == (k % 2), "a single-packet frame had the wrong frame ID");
ck(got_flag[k] == ((k % 3 == 0) ? 1 : 0),
"a single-packet frame lost its error flag");
end
end
// =============================================================
// PHASE 5 (RANDOM)
// =============================================================
`ifndef DIRECTED_ONLY
for (k = 0; k < 400; k = k + 1) begin
P = 2 + (urand() % 7);
li = (urand() % 3 == 0) ? -1 : (urand() % (2 * P));
run_seq(P, li, (urand() % 3) * 8, (urand() % 5) == 0);
end
`endif
nr = 0; for (ri = 0; ri < 102; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/102 errors=%0d",
steps, checks, nr, errors);
$display("[uvc] packets=%0d frames=%0d truncated=%0d flagged_frames=%0d",
c_pkt, c_frame, c_trunc, c_flag);
$display("[uvc] losses injected=%0d detected=%0d SILENT=%0d clean runs=%0d",
t_losses, t_detected, t_silent, t_clean);
$display("--- what fraction of a single packet loss is even visible? ---");
$display(" packets/frame losses detectable silent detectable %%");
for (k = 0; k < 4; k = k + 1)
$display(" %13d %8d %12d %8d %11d",
tab_p[k], tab_total[k], tab_det[k], tab_total[k] - tab_det[k],
(tab_det[k] * 100) / tab_total[k]);
if (nr != 102) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmodule7. VHDL-2008
-- =====================================================================
-- uvc_frame_asm -- VHDL-2008.
--
-- CLASSIFICATION: simplified synthesisable teaching RTL.
-- Same hardware contract as the Verilog and SystemVerilog files: same
-- ports, same widths, same reset values, same cycle-by-cycle behaviour.
--
-- This is NOT a webcam. There is no sensor, no compression and no format
-- negotiation. It is the part of a UVC receiver that decides where one
-- frame ends and the next begins.
--
-- A webcam streams over ISOCHRONOUS transfers, and isochronous means
-- exactly one thing: bandwidth is reserved and delivery is not. No ACK,
-- no NAK, no retry. A packet that goes missing is gone, and nothing on
-- the bus will ever mention it again.
--
-- UVC gives the receiver two bits to reconstruct the frame structure:
--
-- FID a frame-ID bit that TOGGLES between consecutive frames
-- EOF set on the last packet of a frame
--
-- There is no per-packet sequence number and no frame length field. So a
-- lost packet is detectable ONLY if it happened to carry EOF:
--
-- * lose the EOF packet -> the frame never terminates, and the next
-- frame's FID toggle ends it instead. DETECTABLE.
-- * lose any other packet -> the frame still gets its EOF, is short by
-- the lost bytes, and nothing says so. SILENT.
--
-- One packet in P carries EOF, so the detectable fraction is 1/P.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uvc_frame_asm is
port (
clk : in std_logic;
rst_n : in std_logic;
-- one isochronous packet that ARRIVED. Packets that were lost are
-- simply never presented; the module has no way to know they existed,
-- which is the whole situation being modelled.
pkt_valid : in std_logic;
pkt_fid : in std_logic; -- toggles per frame
pkt_eof : in std_logic; -- last packet
pkt_err : in std_logic; -- device says this is bad
pkt_bytes : in std_logic_vector(15 downto 0);
frame_done : out std_logic;
frame_bytes : out std_logic_vector(31 downto 0);
frame_fid : out std_logic;
-- Did this frame end the way a frame is supposed to end? The ONLY
-- evidence of loss the receiver ever gets.
frame_eof : out std_logic;
frame_flagged : out std_logic;
n_pkt : out std_logic_vector(31 downto 0);
n_frame : out std_logic_vector(31 downto 0);
n_truncated : out std_logic_vector(31 downto 0);
n_flagged : out std_logic_vector(31 downto 0);
n_toggle : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of uvc_frame_asm is
signal started : std_logic := '0'; -- a frame is currently open
signal cur_fid : std_logic := '0'; -- the FID of the open frame
signal acc : unsigned(31 downto 0) := (others => '0');
signal acc_err : std_logic := '0';
signal done_r, fid_r, eof_r, flag_r : std_logic := '0';
signal bytes_r : unsigned(31 downto 0) := (others => '0');
signal pkt_c, frame_c, trunc_c, flag_c, tog_c
: unsigned(31 downto 0) := (others => '0');
-- A packet whose FID differs from the open frame's means the previous
-- frame ended without ever presenting EOF. That is the only loss signal
-- this protocol has.
signal fid_toggled : std_logic;
begin
fid_toggled <= '1' when (started = '1' and pkt_fid /= cur_fid) else '0';
frame_done <= done_r;
frame_bytes <= std_logic_vector(bytes_r);
frame_fid <= fid_r;
frame_eof <= eof_r;
frame_flagged <= flag_r;
n_pkt <= std_logic_vector(pkt_c);
n_frame <= std_logic_vector(frame_c);
n_truncated <= std_logic_vector(trunc_c);
n_flagged <= std_logic_vector(flag_c);
n_toggle <= std_logic_vector(tog_c);
process (clk, rst_n)
begin
if rst_n = '0' then
started <= '0';
cur_fid <= '0';
acc <= (others => '0');
acc_err <= '0';
done_r <= '0';
fid_r <= '0';
eof_r <= '0';
flag_r <= '0';
bytes_r <= (others => '0');
pkt_c <= (others => '0');
frame_c <= (others => '0');
trunc_c <= (others => '0');
flag_c <= (others => '0');
tog_c <= (others => '0');
elsif rising_edge(clk) then
done_r <= '0';
if pkt_valid = '1' then
pkt_c <= pkt_c + 1;
if fid_toggled = '1' then
-- ---- the previous frame ended the WRONG way ----
--
-- It never showed EOF, so the packet carrying EOF did not
-- arrive. Close it out and report it as truncated: this is the
-- one loss the receiver can actually see.
done_r <= '1';
bytes_r <= acc;
fid_r <= cur_fid;
eof_r <= '0';
flag_r <= acc_err;
frame_c <= frame_c + 1;
trunc_c <= trunc_c + 1;
tog_c <= tog_c + 1;
-- and the arriving packet opens the next one
started <= '1';
cur_fid <= pkt_fid;
acc <= resize(unsigned(pkt_bytes), 32);
acc_err <= pkt_err;
if pkt_err = '1' then flag_c <= flag_c + 1; end if;
elsif started = '0' then
-- ---- first packet of a new frame ----
started <= '1';
cur_fid <= pkt_fid;
acc <= resize(unsigned(pkt_bytes), 32);
acc_err <= pkt_err;
if pkt_err = '1' then flag_c <= flag_c + 1; end if;
if pkt_eof = '1' then
-- a single-packet frame
done_r <= '1';
bytes_r <= resize(unsigned(pkt_bytes), 32);
fid_r <= pkt_fid;
eof_r <= '1';
flag_r <= pkt_err;
frame_c <= frame_c + 1;
started <= '0';
end if;
else
-- ---- a continuation packet of the open frame ----
acc <= acc + resize(unsigned(pkt_bytes), 32);
acc_err <= acc_err or pkt_err;
if pkt_err = '1' then flag_c <= flag_c + 1; end if;
if pkt_eof = '1' then
-- ---- the frame ended the RIGHT way ----
--
-- Note what is NOT checked here, because it cannot be: there
-- is no length field and no sequence number, so a frame short
-- by one lost packet arrives here looking exactly like a
-- complete one.
done_r <= '1';
bytes_r <= acc + resize(unsigned(pkt_bytes), 32);
fid_r <= cur_fid;
eof_r <= '1';
flag_r <= acc_err or pkt_err;
frame_c <= frame_c + 1;
started <= '0';
end if;
end if;
end if;
end if;
end process;
end architecture;The VHDL testbench
-- =====================================================================
-- Testbench for uvc_frame_asm -- VHDL-2008.
--
-- THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
--
-- That asymmetry is the experiment. The bench generates a frame
-- sequence, decides which packet to drop, presents only the survivors,
-- and compares the design's view of each frame against the truth it is
-- holding back. Three outcomes per frame:
--
-- CLEAN every packet arrived; bytes match the truth
-- SILENT a packet was lost mid-frame; the EOF still arrived, so
-- the frame looks complete and is short. The DESIGN cannot
-- tell. The bench can.
-- DETECTED the EOF packet itself was lost, so the frame ended on
-- the next frame's FID toggle. The design CAN tell.
--
-- THIS IS THE INDEPENDENT BENCH. The directed phases are structurally
-- identical to the Verilog and SystemVerilog benches, so the DIRECTED
-- mutation columns must agree EXACTLY. The random phase uses a
-- VHDL-native generator.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_uv_vhdl is
generic (
DIRECTED_ONLY : boolean := false
);
end entity;
architecture sim of tb_uv_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal pkt_valid : std_logic := '0';
signal pkt_fid : std_logic := '0';
signal pkt_eof : std_logic := '0';
signal pkt_err : std_logic := '0';
signal pkt_bytes : std_logic_vector(15 downto 0) := (others => '0');
signal frame_done : std_logic;
signal frame_bytes : std_logic_vector(31 downto 0);
signal frame_fid : std_logic;
signal frame_eof : std_logic;
signal frame_flagged : std_logic;
signal n_pkt, n_frame, n_truncated, n_flagged, n_toggle
: std_logic_vector(31 downto 0);
begin
dut : entity work.uvc_frame_asm
port map (
clk => clk, rst_n => rst_n,
pkt_valid => pkt_valid, pkt_fid => pkt_fid, pkt_eof => pkt_eof,
pkt_err => pkt_err, pkt_bytes => pkt_bytes,
frame_done => frame_done, frame_bytes => frame_bytes,
frame_fid => frame_fid, frame_eof => frame_eof,
frame_flagged => frame_flagged,
n_pkt => n_pkt, n_frame => n_frame, n_truncated => n_truncated,
n_flagged => n_flagged, n_toggle => n_toggle
);
clkgen : process
begin
while not done loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
main : process
variable errors : integer := 0;
variable checks : integer := 0;
variable steps : integer := 0;
variable lo : line;
-- GROUND TRUTH, invisible to the design
-- Sized for the longest sequence any phase runs. Phase 3 streams
-- sixteen frames; an array of eight made that a fatal index error.
type i8_t is array (0 to 31) of integer;
variable true_bytes : i8_t := (others => 0);
variable sent_bytes : i8_t := (others => 0);
variable true_eof : i8_t := (others => 0);
-- what the design reported, captured AS it reports it
variable got_n : integer := 0;
variable got_bytes, got_fid, got_eof, got_flag : i8_t := (others => 0);
-- the measurement
-- m_* are PER-RUN flags: phase 2 zeroes them before each sequence so
-- it can ask whether that particular loss was detectable. t_* are the
-- cumulative totals, which nothing zeroes.
variable m_clean, m_silent, m_detected, m_losses : integer := 0;
variable t_clean, t_silent, t_detected, t_losses : integer := 0;
type i5_t is array (0 to 4) of integer;
variable tab_p, tab_total, tab_det : i5_t := (others => 0);
-- cumulative across resets
variable c_pkt, c_frame, c_trunc, c_flag : integer := 0;
type reach_t is array (0 to 101) of boolean;
variable reach : reach_t := (others => false);
variable nr : integer := 0;
type i4_t is array (0 to 3) of integer;
constant PVAL : i4_t := (2, 4, 8, 16);
variable rnd_state : unsigned(31 downto 0) := x"00120702";
impure function urand return integer is
begin
rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
+ to_unsigned(12345, 32);
-- HIGH bits: in an LCG with a power-of-two modulus bit i has period
-- 2**(i+1), so the low bits cycle in lockstep with the calling loop
-- while their histogram stays perfectly uniform.
return to_integer(rnd_state(30 downto 15));
end function;
-- The frame-ID bit for frame k, as a named function. The obvious
-- alternative -- slicing bit 0 out of a one-bit to_unsigned conversion
-- -- is a TRUNCATING conversion for every k above 1, and nvc warns on
-- every call.
function fid_of (k : integer) return std_logic is
begin
if (k mod 2) = 1 then return '1'; else return '0'; end if;
end function;
procedure ck (cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(lo, string'(" ERROR @") & time'image(now) &
string'(" step#") & integer'image(steps) &
string'(": ") & what);
writeline(output, lo);
end if;
end if;
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
pkt_valid <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
got_n := 0;
true_bytes := (others => 0);
sent_bytes := (others => 0);
true_eof := (others => 0);
got_bytes := (others => 0);
got_fid := (others => 0);
got_eof := (others => 0);
got_flag := (others => 0);
end procedure;
-- Present one packet and collect any frame the design closes out.
-- The frame's fields are captured HERE, in the cycle frame_done is
-- asserted; reading them later reads whatever the next frame leaves.
procedure send_pkt (fid, eof, err : std_logic; nbytes : integer) is
begin
pkt_valid <= '1';
pkt_fid <= fid;
pkt_eof <= eof;
pkt_err <= err;
pkt_bytes <= std_logic_vector(to_unsigned(nbytes, 16));
wait until rising_edge(clk);
wait for 1 ns;
pkt_valid <= '0';
c_pkt := c_pkt + 1;
if frame_done = '1' then
c_frame := c_frame + 1;
if frame_eof = '0' then c_trunc := c_trunc + 1; end if;
if frame_flagged = '1' then c_flag := c_flag + 1; end if;
if got_n < 32 then
got_bytes(got_n) := to_integer(unsigned(frame_bytes));
if frame_fid = '1' then got_fid(got_n) := 1; else got_fid(got_n) := 0; end if;
if frame_eof = '1' then got_eof(got_n) := 1; else got_eof(got_n) := 0; end if;
if frame_flagged = '1' then got_flag(got_n) := 1; else got_flag(got_n) := 0; end if;
end if;
got_n := got_n + 1;
end if;
steps := steps + 1;
end procedure;
procedure drain is
begin
for g in 0 to 3 loop
wait until rising_edge(clk);
wait for 1 ns;
if frame_done = '1' then
c_frame := c_frame + 1;
if frame_eof = '0' then c_trunc := c_trunc + 1; end if;
if frame_flagged = '1' then c_flag := c_flag + 1; end if;
if got_n < 32 then
got_bytes(got_n) := to_integer(unsigned(frame_bytes));
if frame_fid = '1' then got_fid(got_n) := 1; else got_fid(got_n) := 0; end if;
if frame_eof = '1' then got_eof(got_n) := 1; else got_eof(got_n) := 0; end if;
if frame_flagged = '1' then got_flag(got_n) := 1; else got_flag(got_n) := 0; end if;
end if;
got_n := got_n + 1;
end if;
end loop;
end procedure;
-- -----------------------------------------------------------------
-- Three frames of P packets, dropping ONE if `lost` >= 0.
--
-- Frames 0 and 1 are the ones a loss is injected into, so there is
-- always a following frame whose FID toggle could reveal a missing
-- EOF. Frame 2 always completes.
-- -----------------------------------------------------------------
procedure run_seq (P : integer; lost : integer; base : integer;
errbit : boolean) is
variable idx, sz : integer;
variable e_det, e_silent : integer;
variable lost_sz, lost_was_eof : integer;
variable eb : std_logic;
begin
reset_dut;
lost_sz := 0; lost_was_eof := 0;
idx := 0;
for f in 0 to 2 loop
-- `pp`, not `p`: VHDL is case-insensitive, so a loop variable `p`
-- hides the procedure's own parameter `P`.
for pp in 0 to P - 1 loop
sz := base + ((pp mod 4) * 4);
true_bytes(f) := true_bytes(f) + sz;
if idx /= lost then
sent_bytes(f) := sent_bytes(f) + sz;
if pp = P - 1 then true_eof(f) := 1; end if;
if errbit and pp = 0 then eb := '1'; else eb := '0'; end if;
if pp = P - 1 then
send_pkt(fid_of(f), '1', eb, sz);
else
send_pkt(fid_of(f), '0', eb, sz);
end if;
else
-- remember what was thrown away, so the damage can be
-- predicted rather than merely bounded
lost_sz := sz;
if pp = P - 1 then lost_was_eof := 1; end if;
end if;
idx := idx + 1;
end loop;
end loop;
drain;
ck(got_n = 3, "the wrong number of frames was reported");
e_det := 0; e_silent := 0;
for f in 0 to 2 loop
if f < got_n then
-- ---- PROPERTY 1: the FID alternates, frame by frame ----
ck(got_fid(f) = (f mod 2),
"a frame was reported with the wrong frame ID");
-- ---- PROPERTY 2: frame_eof says whether the EOF arrived ----
ck(got_eof(f) = true_eof(f),
"frame_eof does not say whether the EOF packet arrived");
-- ---- PROPERTY 3: the byte count is what ARRIVED ----
ck(got_bytes(f) = sent_bytes(f),
"the frame byte count does not match the bytes that arrived");
if sent_bytes(f) /= true_bytes(f) or true_eof(f) = 0 then
if got_eof(f) = 0 then e_det := e_det + 1;
else e_silent := e_silent + 1; end if;
end if;
end if;
end loop;
if lost >= 0 then
m_losses := m_losses + 1;
-- ---- PROPERTY 4: the damage is exactly predictable ----
--
-- Which of the three outcomes occurs is decided by two facts the
-- bench knows about the packet it threw away: whether it carried
-- EOF, and whether it carried any bytes. A zero-length packet that
-- was not the last one costs nothing at all -- which is not a
-- loophole, it is how a device says "nothing this microframe"
-- while keeping its bandwidth reservation alive.
if lost_was_eof = 1 then
ck(e_det = 1 and e_silent = 0,
"losing the EOF packet was not reported as a truncated frame");
elsif lost_sz > 0 then
ck(e_silent = 1 and e_det = 0,
"losing a mid-frame packet was not a silently short frame");
else
ck(e_det + e_silent = 0, "losing a zero-length packet damaged a frame");
end if;
if e_det > 0 then m_detected := m_detected + 1; t_detected := t_detected + 1; end if;
if e_silent > 0 then m_silent := m_silent + 1; t_silent := t_silent + 1; end if;
t_losses := t_losses + 1;
else
-- ---- PROPERTY 5: with nothing lost, nothing is damaged ----
--
-- The false-positive half. A detector that flags loss on a clean
-- stream gets switched off, and then detects nothing at all.
ck(e_det + e_silent = 0,
"a frame was reported damaged on a stream with no loss");
m_clean := m_clean + 1; t_clean := t_clean + 1;
end if;
end procedure;
variable ri, P, li, tot : integer;
begin
reset_dut;
-- ===============================================================
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
-- 17 loss positions x 2 error bits x 3 payload profiles = 102.
-- ===============================================================
for lx in -1 to 15 loop
for eb in 0 to 1 loop
for bs in 0 to 2 loop
run_seq(8, lx, bs * 8, eb = 1);
ri := ((lx + 1) * 2 + eb) * 3 + bs;
reach(ri) := true;
end loop;
end loop;
end loop;
-- ===============================================================
-- PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
--
-- For each packets-per-frame, drop every droppable packet in turn
-- and count how many of those losses the receiver could detect.
-- Exactly one packet per frame carries EOF, so exactly one loss per
-- frame is visible: the sweep measures that rather than asserting it.
-- ===============================================================
for pi in 0 to 3 loop
P := PVAL(pi);
tab_p(pi) := P;
for lx in 0 to 2 * P - 1 loop
m_detected := 0; m_silent := 0;
run_seq(P, lx, 8, false);
tab_total(pi) := tab_total(pi) + 1;
if m_detected > 0 then tab_det(pi) := tab_det(pi) + 1; end if;
end loop;
end loop;
m_detected := 0; m_silent := 0; m_losses := 0; m_clean := 0;
-- ===============================================================
-- PHASE 3 (DIRECTED) -- a long clean stream, the false-positive
-- test. A loss detector that fires here is worse than none.
-- ===============================================================
reset_dut;
for k in 0 to 15 loop
tot := 0;
for p2 in 0 to 3 loop
tot := tot + 8 + p2;
if p2 = 3 then
send_pkt(fid_of(k), '1', '0', 8 + p2);
else
send_pkt(fid_of(k), '0', '0', 8 + p2);
end if;
end loop;
drain;
ck(got_n = k + 1, "a clean stream lost a frame");
if got_n = k + 1 then
ck(got_eof(k) = 1, "a clean frame did not end on EOF");
ck(got_bytes(k) = tot, "a clean frame reported the wrong byte count");
ck(got_fid(k) = (k mod 2), "a clean frame had the wrong frame ID");
end if;
end loop;
-- ===============================================================
-- PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
--
-- The one loss signal that does NOT depend on the framing bits: a
-- device that knows it dropped data can say so. Swept over which
-- packet carries it, with a no-flag control -- without the control
-- the property would be satisfied by a receiver that flagged
-- everything.
-- ===============================================================
for pi in 0 to 2 loop
for k in -1 to PVAL(pi) - 1 loop
reset_dut;
P := PVAL(pi);
for p3 in 0 to P - 1 loop
if p3 = P - 1 then
if p3 = k then send_pkt('0', '1', '1', 8);
else send_pkt('0', '1', '0', 8); end if;
else
if p3 = k then send_pkt('0', '0', '1', 8);
else send_pkt('0', '0', '0', 8); end if;
end if;
end loop;
drain;
ck(got_n = 1, "the flagged frame was not reported");
if got_n = 1 then
-- ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
if k >= 0 then
ck(got_flag(0) = 1,
"the frame error flag does not reflect the packet that carried it");
else
ck(got_flag(0) = 0,
"the frame error flag does not reflect the packet that carried it");
end if;
end if;
end loop;
end loop;
-- ===============================================================
-- PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
--
-- A frame whose first packet also carries EOF takes a branch nothing
-- else reaches. Not a corner case invented for coverage: a
-- still-image capture arrives on the same endpoint and is routinely
-- a single payload.
-- ===============================================================
reset_dut;
for k in 0 to 7 loop
if k mod 3 = 0 then send_pkt(fid_of(k), '1', '1', 64);
else send_pkt(fid_of(k), '1', '0', 64); end if;
ck(got_n = k + 1, "a single-packet frame was not closed out");
if got_n = k + 1 then
ck(got_eof(k) = 1, "a single-packet frame did not end on EOF");
ck(got_bytes(k) = 64, "a single-packet frame reported the wrong size");
ck(got_fid(k) = (k mod 2), "a single-packet frame had the wrong frame ID");
if k mod 3 = 0 then
ck(got_flag(k) = 1, "a single-packet frame lost its error flag");
else
ck(got_flag(k) = 0, "a single-packet frame lost its error flag");
end if;
end if;
end loop;
-- ===============================================================
-- PHASE 5 (RANDOM)
-- ===============================================================
if not DIRECTED_ONLY then
for k in 0 to 399 loop
P := 2 + (urand mod 7);
if (urand mod 3) = 0 then li := -1; else li := urand mod (2 * P); end if;
run_seq(P, li, (urand mod 3) * 8, (urand mod 5) = 0);
end loop;
end if;
nr := 0;
for i in 0 to 101 loop
if reach(i) then nr := nr + 1; end if;
end loop;
write(lo, string'("steps=") & integer'image(steps) &
string'(" checks=") & integer'image(checks) &
string'(" reach=") & integer'image(nr) &
string'("/102 errors=") & integer'image(errors));
writeline(output, lo);
write(lo, string'("[uvc] packets=") & integer'image(c_pkt) &
string'(" frames=") & integer'image(c_frame) &
string'(" truncated=") & integer'image(c_trunc) &
string'(" flagged_frames=") & integer'image(c_flag));
writeline(output, lo);
write(lo, string'("[uvc] losses injected=") & integer'image(t_losses) &
string'(" detected=") & integer'image(t_detected) &
string'(" SILENT=") & integer'image(t_silent) &
string'(" clean runs=") & integer'image(t_clean));
writeline(output, lo);
write(lo, string'("--- what fraction of a single packet loss is even visible? ---"));
writeline(output, lo);
write(lo, string'(" packets/frame losses detectable silent detectable %"));
writeline(output, lo);
for k in 0 to 3 loop
write(lo, string'(" "));
write(lo, tab_p(k), right, 13);
write(lo, tab_total(k), right, 9);
write(lo, tab_det(k), right, 13);
write(lo, tab_total(k) - tab_det(k), right, 9);
write(lo, (tab_det(k) * 100) / tab_total(k), right, 12);
writeline(output, lo);
end loop;
if nr /= 102 then
write(lo, string'("FAIL: exhaustive sweep incomplete")); writeline(output, lo);
errors := errors + 1;
end if;
if errors = 0 then
write(lo, string'("PASS: 0 errors in ") & integer'image(checks) & string'(" checks"));
else
write(lo, string'("FAIL: ") & integer'image(errors) &
string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, lo);
done <= true;
wait;
end process;
end architecture;8. Assertions
// ---------------------------------------------------------------------
// Properties for uvc_frame_asm.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
// concurrent assertions, so every number published here comes from the
// procedural checks in the testbenches.
//
// Read the list and notice what is MISSING: there is no property saying
// a frame is complete. There cannot be one. The module has no length to
// check against, and inventing an assertion that pretends otherwise
// would be asserting something the protocol does not support.
// ---------------------------------------------------------------------
module uvc_frame_sva (
input logic clk,
input logic rst_n,
input logic pkt_valid,
input logic pkt_fid,
input logic pkt_eof,
input logic pkt_err,
input logic [15:0] pkt_bytes,
input logic frame_done,
input logic [31:0] frame_bytes,
input logic frame_fid,
input logic frame_eof,
input logic frame_flagged,
input logic [31:0] n_frame,
input logic [31:0] n_truncated
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. frame_done is a single-cycle pulse ----
// A frame announced for two cycles is counted twice by any consumer
// that samples the flag.
a_done_pulse : assert property (frame_done |=> !frame_done);
// ---- 2. a frame only ever closes on a packet ----
// Nothing closes a frame except an arriving packet: there is no
// timeout in this module, because a timeout would need a clock the
// protocol does not define.
a_done_needs_pkt : assert property (frame_done |-> $past(pkt_valid));
// ---- 3. a frame that ends on EOF ends because EOF arrived ----
//
// THE property. frame_eof is the receiver's only loss signal, so it
// must mean exactly one thing and never approximate it.
a_eof_honest : assert property
((frame_done && frame_eof) |-> $past(pkt_eof));
// ---- 4. a frame that ends WITHOUT EOF ends on a FID toggle ----
// The converse half. Together, 3 and 4 say frame_eof is exactly the
// distinction between the two terminations and nothing else.
a_trunc_honest : assert property
((frame_done && !frame_eof) |-> ($past(pkt_valid) && ($past(pkt_fid) != frame_fid)));
// ---- 5. the truncated counter tracks the truncated frames ----
// Stated in this direction: the counter may not move for any other
// reason. The published loss figures are read off these counters, so a
// counter that drifted would make every measurement unfalsifiable.
a_trunc_count : assert property
((n_truncated != $past(n_truncated)) |-> ($past(frame_done) && !$past(frame_eof)));
// ---- 6. a flagged frame contained a flagged packet ----
// The device's own error bit is the one loss signal that does NOT
// depend on the framing bits, so it must survive from any packet of the
// frame to the frame report.
a_flag_grounded : assert property
((frame_done && frame_flagged) |-> $past(pkt_err) or 1'b1);
// (Written loosely here: the real obligation is over the whole frame
// and needs an auxiliary accumulator, which a formal tool would carry.)
// ---- 7. the byte count never exceeds what arrived ----
// A frame reporting more bytes than were delivered would be inventing
// pixels. Bounded against a running total a formal tool would maintain.
// ---- COVER: both terminations, and a zero-length packet ----
// A suite that only ever sees clean frames has tested nothing about
// loss, and one that never sees a zero-length packet has missed the
// case where losing a packet costs nothing.
c_eof : cover property (frame_done && frame_eof);
c_trunc : cover property (frame_done && !frame_eof);
c_flagged : cover property (frame_done && frame_flagged);
c_zlp : cover property (pkt_valid && (pkt_bytes == 0));
c_single : cover property (pkt_valid && pkt_eof && frame_done);
endmodule9. Where UVM Fits
// ---------------------------------------------------------------------
// UVM structure for UVC frame assembly.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
// on virtual method dispatch -- so every number comes from the
// procedural benches.
//
// THE DESIGN DECISION: loss is injected by the AGENT, not by the
// sequence. The sequence describes a perfect video stream; the agent
// decides what fails to arrive. That separation is what lets the
// scoreboard hold ground truth -- it is fed the sequence's intent, while
// the monitor sees only the survivors.
// ---------------------------------------------------------------------
class uvc_packet extends uvm_sequence_item;
`uvm_object_utils(uvc_packet)
rand bit fid;
rand bit eof;
rand bit err;
rand int nbytes;
constraint c_size {
// Zero-length packets are a real and frequent case: they are how a
// device holds its bandwidth reservation with nothing to send. A
// constraint of nbytes > 0 would delete the one loss that costs
// nothing, and with it the reason the bench's damage prediction has
// three arms instead of two.
nbytes inside {0, [8:1024]};
}
function new(string name = "uvc_packet");
super.new(name);
endfunction
endclass
// ---- the agent drops packets; the sequence never knows ----
//
// A driver that can decide not to drive is unusual and is the whole point
// here: the DUT must experience a gap with no marker of any kind, which
// is exactly what a real isochronous loss looks like.
class iso_driver extends uvm_driver #(uvc_packet);
`uvm_component_utils(iso_driver)
virtual uvc_if vif;
// Loss rate as a per-mille figure, so a realistic 0.1% is expressible.
// A regression run at 10% loss measures a broken link, not a webcam.
int unsigned loss_per_mille = 1;
uvm_analysis_port #(uvc_packet) sent_ap; // what was INTENDED
uvm_analysis_port #(uvc_packet) wire_ap; // what was DRIVEN
task run_phase(uvm_phase phase);
forever begin
uvc_packet p;
seq_item_port.get_next_item(p);
sent_ap.write(p); // truth, always
if (($urandom_range(999) >= loss_per_mille)) begin
drive(p);
wire_ap.write(p); // only the survivors
end
seq_item_port.item_done();
end
endtask
task drive(uvc_packet p);
vif.pkt_valid <= 1'b1;
vif.pkt_fid <= p.fid;
vif.pkt_eof <= p.eof;
vif.pkt_err <= p.err;
vif.pkt_bytes <= p.nbytes[15:0];
@(posedge vif.clk);
vif.pkt_valid <= 1'b0;
endtask
endclass
// ---- the scoreboard holds both streams and measures the difference ----
class uvc_scoreboard extends uvm_scoreboard;
`uvm_component_utils(uvc_scoreboard)
// fed from sent_ap: what the camera meant to send
int unsigned truth_bytes[$];
// fed from the DUT's frame reports: what the receiver believes
int unsigned n_clean, n_silent, n_detected;
function void report_phase(uvm_phase phase);
`uvm_info("UVC", $sformatf(
"frames: %0d clean, %0d SILENTLY SHORT, %0d detected as truncated",
n_clean, n_silent, n_detected), UVM_LOW)
// THE measurement, and the reason this environment exists. A
// regression that reports only a pass/fail has thrown away the number
// the chapter is about.
if (n_silent + n_detected > 0)
`uvm_info("UVC", $sformatf("detectable fraction: %0d%%",
(n_detected * 100) / (n_silent + n_detected)), UVM_LOW)
// A silently short frame is NOT a DUT error -- the DUT reported what
// it received. Flagging it as one would make the environment fail on
// correct behaviour, and it would be switched off within a week.
endfunction
endclass
// ---- coverage: the loss POSITION, not the loss rate ----
class uvc_coverage extends uvm_subscriber #(uvc_packet);
`uvm_component_utils(uvc_coverage)
int pkt_index_in_frame;
int frame_length_pkts;
covergroup cg with function sample(int idx, int len, bit was_lost);
// Which packet of the frame was lost. THE coverpoint: losing the last
// one is a different event from losing any other, and a regression
// that never lost a last packet has never exercised the detection
// path at all.
cp_pos : coverpoint idx { bins first = {0}; bins middle = {[1:30]}; bins last = {31}; }
cp_len : coverpoint len { bins short = {[1:4]}; bins med = {[5:32]}; bins long = {[33:1000]}; }
cp_lost : coverpoint was_lost;
x_which : cross cp_pos, cp_lost;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new();
endfunction
function void write(uvc_packet t);
cg.sample(pkt_index_in_frame, frame_length_pkts, 1'b0);
endfunction
endclass10. Mutation Testing
Eight defects, injected one at a time into all three languages. Every replacement asserted; each mutation generated as its own file.
| # | the injected defect | V-all | V-dir | SV-all | SV-dir | VHDL-all | VHDL-dir |
|---|---|---|---|---|---|---|---|
| BASE | unmodified design | 0 | 0 | 0 | 0 | 0 | 0 |
| Q1 | a truncated frame is reported as normal | 182 | 40 | 182 | 40 | 178 | 40 |
| Q2 | the FID is never compared | 411 | 90 | 411 | 90 | 398 | 90 |
| Q3 | a truncated frame reports the wrong bytes | 65 | 18 | 65 | 18 | 63 | 18 |
| Q4 | the closing packet's payload is dropped | 1532 | 480 | 1532 | 480 | 1543 | 480 |
| Q5 | the accumulator is not reloaded on a toggle | 80 | 20 | 80 | 20 | 78 | 20 |
| Q6 | the frame stays open after EOF | 2912 | 928 | 2912 | 928 | 2875 | 928 |
| Q7 | the device's error flag is not carried | 8 | 8 | 8 | 8 | 8 | 8 |
| Q8 | a single-packet frame is not recognised | 28 | 12 | 28 | 12 | 28 | 12 |
Every mutation is killed, and every one by directed stimulus alone. The directed column is identical across all three languages at all eight rows.
Q1 is the mutation this chapter was built for
Q1 changes one bit: a frame closed by a FID toggle reports frame_eof = 1
instead of 0. The assembler still works perfectly — right frames, right
boundaries, right byte counts. All it stops doing is saying that a frame
ended the wrong way.
Since that is the only loss signal the protocol has, Q1 takes the detectable fraction from 1/P to zero. A receiver with this defect reports every frame as complete, forever, under any loss rate.
It scores 40, entirely directed. That number is small, and it is small for the right reason: only 2 losses per sweep are detectable in the first place, so there are only so many opportunities to get it wrong. Divide the score by its domain before calling it weak — 40 failures against 20 detectable losses is every single one of them, caught twice.
Run totals
| steps | checks | reach | losses | detected | silent | errors | |
|---|---|---|---|---|---|---|---|
| Verilog, full | 10,217 | 6,320 | 102 / 102 | 426 | 91 | 305 | 0 |
| Verilog, directed only | 4,502 | 1,920 | 102 / 102 | 156 | 20 | 128 | 0 |
| SystemVerilog, full | 10,217 | 6,320 | 102 / 102 | 426 | 91 | 305 | 0 |
| SystemVerilog, directed only | 4,502 | 1,920 | 102 / 102 | 156 | 20 | 128 | 0 |
| VHDL, full | 10,274 | 6,320 | 102 / 102 | 420 | 89 | 290 | 0 |
| VHDL, directed only | 4,502 | 1,920 | 102 / 102 | 156 | 20 | 128 | 0 |
The directed-only rows are identical across all three languages in every column, including every loss measurement. The full runs differ only in the VHDL stream's own random choices — and note that even there the check count is the same 6,320, because the number of checks per sequence does not depend on what the sequence contains.
11. What This Does Not Cover
No sensor, no compression, no format negotiation. A real camera spends most of its silicon on those. None of them changes the framing.
The payload header is reduced to its two framing bits. Real UVC headers also carry a presentation timestamp, a source clock reference and an end-of-header flag. The timestamp in particular would let a receiver detect some losses — and leaving it out is deliberate, because this chapter is about what FID and EOF alone can tell you.
No bandwidth negotiation. The reserved bandwidth that makes isochronous
isochronous is set up at SET_INTERFACE time by choosing an alternate setting.
That is module 16's subject.
Single losses only. The exhaustive sweep drops exactly one packet. Two losses in one frame behave the same way unless one of them is the EOF packet, but the sweep is single-loss and the numbers should not be read as a multi-loss model.
No still-image path. A UVC device can carry stills on the same endpoint with a different header bit. The single-packet frame case in phase 4b is the shape that path takes, but the negotiation around it is out of scope.
Frames are 2 to 16 packets. Real frames are hundreds. The 1/P relationship is exact and extrapolates directly; the table stops at 16 because an exhaustive single-loss sweep at P = 300 is 600 sequences for one row.
12. The Interview Answer
"How does a USB webcam stream video?" — three sentences.
1. Name the mode and what it costs. "Isochronous transfers: bandwidth is reserved every frame, and delivery is not guaranteed. No ACK, no retry — a lost packet is gone, because a retransmitted video packet would arrive after the frame was displayed anyway."
2. Name the framing. "The receiver reconstructs frames from two bits in the UVC payload header: a frame-ID bit that toggles between frames, and an end-of-frame flag. There is no sequence number and no length field."
3. Name the consequence. "So a lost packet is only detectable if it was the one carrying EOF — one in P. For a frame that is hundreds of packets, better than 99% of losses produce a frame the receiver reports as complete with a hole in it."
The follow-up worth having ready is why that is acceptable: the failure mode is a smeared frame at 30 fps, which a viewer barely registers. Put the same mechanism under a filesystem and it is unusable — which is exactly why chapter 29.1's flash drive uses bulk with a thirteen-case error contract instead.
13. What Carries Forward
Two case studies, two opposite shapes:
| chapter | the shape | the cost |
|---|---|---|
| 29.1, flash drive | a decision table with fatal cells | six of thirteen cases unrecoverable |
| 29.2, webcam | a firehose with no error handling | 1/P of losses detectable |
The next chapter keeps isochronous and changes what is being protected. A webcam can afford to lose a frame. An audio device cannot afford to drift — if the host's clock and the device's differ by one part in a million, a buffer somewhere fills or empties, and it does so relentlessly.
Continue learning
Related tutorials
- Related topic
USB Audio Devices
An audio device runs on its own crystal, so a few parts per million empty the buffer every minute — with nothing lost and nothing to retry. The fix is one 10.14 number per frame, and the steady-state offset is a closed form: crystal error × 2^gain.
- Related topic
USB in Embedded Devices
On a microcontroller the controller is a peripheral, and the protocol can be perfectly correct while the device goes deaf. Everything turns on one question — who owns this buffer right now — answered by one bit per buffer and exhausted over 132 transitions in three languages.
- Related topic
Video over Isochronous
A video frame is many isochronous packets, and losing the one carrying End-of-Frame would merge frames forever. The single toggling bit that prevents it — and the mutation that exposed a real defect in the RTL.
- Related topic
USB vs UART
UART spends zero wires on synchronisation and pays a tolerance budget that shrinks as the frame grows; USB spends a SYNC field, an encoding rule and a PLL to buy that budget away — measured across 5376 exhaustive points, not quoted.
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.
