USB · Module 29
Smartphones over USB
A phone is five device classes at once and negotiates which. None of it starts until two resistors answer three questions — and the reversible connector turns the answer into a symmetry, verified exhaustively over 81 transitions.
The fourth case study, and the one where the device refuses to be a single thing. A flash drive is one class. A webcam is one class. An audio device is one class with an awkward clock. A phone is a mass-storage-ish device, a camera, a serial console, possibly a display source, and either a power sink or a power source — and which of those it is on any given day is negotiated.
1. The Phone Is Five Devices And Has To Agree Which
Plug a phone into a laptop and the notification asks what the cable is for. That question is not a user-interface nicety; it is the visible end of a negotiation, and the options are genuinely different device classes.
MTP / PTP a media-transfer or camera class. NOT mass storage --
the phone keeps its filesystem and serves objects, which
is why you cannot chkdsk a phone
ADB a vendor-specific bulk interface: a serial console and a
file pipe for developers
RNDIS / NCM a network interface: tethering
DisplayPort an ALTERNATE MODE -- the USB pairs are handed over to a
completely different protocol
power sink, or source, or either, and at a negotiated voltageA phone presents several of those at once, as a composite device, and changes which on request. All of it is negotiated over the wire.
What rests on the attach decision
2. Two Pins, Three Levels, Three Answers
A source pulls both CC lines up through Rp. A sink pulls its one connected CC line down through Rd. A powered cable identifies itself with Ra on the other line. The source reads each line as one of three levels:
OPEN nothing pulling down -- nothing on this line
Ra pulled down weakly -- a powered (e-marked) cable
Rd pulled down harder -- a SINK is on this lineTwo lines, three levels each, so nine combinations — and five of the nine are an attach:
CC1 CC2 means
---- ---- -----------------------------------------------------
OPEN OPEN nothing
OPEN Ra a powered cable with nothing on the end of it
OPEN Rd a sink, on CC2
Ra OPEN a powered cable with nothing on the end of it
Ra Ra likewise, both ends marked
Ra Rd a sink on CC2, through a powered cable
Rd OPEN a sink, on CC1
Rd Ra a sink on CC1, through a powered cable
Rd Rd NOT a sink -- a debug accessoryAnd the orientation falls out of the same rule for free. Because the plug is reversible, the sink's Rd lands on CC1 or CC2 depending on which way the user inserted it — so which line it is is the orientation. There is no other way to learn it, and nothing else in the system needs to be told.
3. The Design (Verilog-2005)
From two voltages to three answers, with a debounce in the middle
// =====================================================================
// typec_cc_fsm -- the Configuration Channel state machine that decides
// whether anything is plugged in, which way round it is, and how much
// current it is allowed to draw.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a Type-C port controller. There is no VBUS switch, no
// Power Delivery protocol engine, no BMC PHY, no VCONN switching and no
// alternate-mode entry. It is the CC attach machine: the part that runs
// before any of that exists, and that everything else depends on.
//
// WHY THIS IS THE MECHANISM WORTH BUILDING FOR "PHONE ON A CABLE"
// ---------------------------------------------------------------
// A phone on a USB-C cable is simultaneously a mass-storage-ish device
// (MTP), a camera (PTP), a serial console (ADB), possibly a display
// source (DisplayPort alt mode), and either a power sink or a power
// source. Which of those it is gets NEGOTIATED, and none of that
// negotiation can start until three questions are answered:
//
// 1. IS ANYTHING THERE? -- and is it stable, or is it a
// contact bouncing as the plug seats
// 2. WHICH WAY ROUND IS IT? -- the connector is reversible, so the
// same pin is TX in one orientation
// and RX in the other
// 3. HOW MUCH CURRENT? -- advertised as a resistor value
// before a single bit is exchanged
//
// All three are answered by two pins and some resistors, and the answers
// are read as VOLTAGES rather than as a protocol. That is deliberate: it
// works with a dumb cable, a dumb charger and no firmware running.
//
// THE CC LINES
// ------------
// A source (DFP) pulls both CC lines up through Rp. A sink (UFP) pulls
// its one connected CC line down through Rd. A powered cable identifies
// itself with Ra on the other line. The source reads each line as one of
// three levels:
//
// OPEN nothing pulling down -- nothing on this line
// Ra pulled down weakly -- a powered (e-marked) cable
// Rd pulled down harder -- a SINK is on this line
//
// Because the plug is reversible, the sink's Rd lands on CC1 or CC2
// depending on which way the user inserted it, and WHICH ONE IT IS is
// the orientation. There is no other way to learn it.
// =====================================================================
module typec_cc_fsm #(
// How many debounce ticks a CC state must persist before it is believed.
// The specification calls this tCCDebounce and requires 100..200 ms. A
// real port ticks at 1 ms and uses 100..200 here; this module uses 10 so
// a simulation can sweep the whole window exhaustively.
parameter integer DEBOUNCE_TICKS = 10
) (
input wire clk,
input wire rst_n,
// One pulse per debounce tick. Everything time-related in this module
// is counted in ticks, never in clock cycles, so the same RTL works at
// any clock frequency.
input wire tick,
// What the comparators say about each CC line, sampled continuously.
// 2'b00 OPEN 2'b01 Ra 2'b10 Rd
input wire [1:0] cc1_st,
input wire [1:0] cc2_st,
// ---- the three answers ----
output wire attached,
// 1'b0 = the sink is on CC1, 1'b1 = the sink is on CC2. Meaningless
// unless `attached`, and held at its last value rather than cleared, so
// that a detach does not momentarily steer a mux the wrong way.
output wire orientation,
// A powered cable was detected alongside the sink. VCONN would be
// supplied on the OTHER line -- the one carrying Ra.
output wire cable_powered,
// Both CC lines pulled down by Rd. That is not a sink; it is a debug
// accessory, and treating it as a sink is a real interoperability bug.
output wire debug_accessory,
output wire [15:0] n_attach,
output wire [15:0] n_detach,
output wire [15:0] n_glitch // attaches that failed to debounce
);
localparam [1:0] L_OPEN = 2'b00, L_RA = 2'b01, L_RD = 2'b10;
localparam [1:0] S_UNATTACHED = 2'd0,
S_ATTACHWAIT = 2'd1,
S_ATTACHED = 2'd2,
S_DETACHWAIT = 2'd3;
reg [1:0] state;
reg [15:0] ticks;
// What is currently being debounced. See sit_now below.
reg [2:0] sit_lat;
reg orient_r, cable_r, debug_r;
reg [15:0] att_c, det_c, gli_c;
// ---- decode the two lines ----
wire cc1_rd = (cc1_st == L_RD);
wire cc2_rd = (cc2_st == L_RD);
wire cc1_ra = (cc1_st == L_RA);
wire cc2_ra = (cc2_st == L_RA);
// EXACTLY ONE line pulled down by Rd is a sink. The exclusive-or is the
// whole rule, and it is what makes the connector reversible: the machine
// does not care which line it is, only that precisely one of them is.
wire one_rd = cc1_rd ^ cc2_rd;
// BOTH lines pulled down by Rd is a debug accessory, not a sink. A port
// that treats it as a sink will drive VBUS into a debug jig.
wire both_rd = cc1_rd & cc2_rd;
wire attach_cond = one_rd | both_rd;
// Which line the sink is on. Only meaningful when exactly one Rd is
// present, which is exactly when it is sampled.
wire orient_now = cc2_rd;
// Ra on the line the sink is NOT on. With both_rd there is no free line,
// so there is no powered cable to find.
wire cable_now = one_rd ? (cc1_rd ? cc2_ra : cc1_ra) : 1'b0;
// ---- the SITUATION, which is what actually gets debounced ----
//
// The specification debounces the CC STATE, not the fact that something
// is attached. Those are different, and the difference is a real bug:
// if Rd moves from CC1 to CC2 part-way through the window -- which is
// exactly what a plug being rocked into place does -- then attach_cond
// never drops, and a machine watching only attach_cond would accept the
// attachment with an orientation that had been stable for one tick.
//
// Bundling the three decoded answers into one value makes "has anything
// about this changed" a single comparison, and makes it impossible to
// debounce one answer and forget another.
wire [2:0] sit_now = {both_rd, cable_now, orient_now};
assign attached = (state == S_ATTACHED) || (state == S_DETACHWAIT);
assign orientation = orient_r;
assign cable_powered = cable_r;
assign debug_accessory = debug_r;
assign n_attach = att_c;
assign n_detach = det_c;
assign n_glitch = gli_c;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_UNATTACHED;
ticks <= 16'd0;
sit_lat <= 3'd0;
orient_r <= 1'b0;
cable_r <= 1'b0;
debug_r <= 1'b0;
att_c <= 16'd0;
det_c <= 16'd0;
gli_c <= 16'd0;
end else begin
case (state)
// -------------------------------------------------------------
S_UNATTACHED: begin
if (attach_cond) begin
state <= S_ATTACHWAIT;
ticks <= 16'd0;
sit_lat <= sit_now;
end
end
// -------------------------------------------------------------
// The condition has to SURVIVE tCCDebounce. This is not
// belt-and-braces: a plug being pushed home makes and breaks
// contact several times over a few milliseconds, and a port that
// believed the first edge would enumerate, drop, and enumerate
// again -- which is the "my phone keeps reconnecting" bug.
// -------------------------------------------------------------
S_ATTACHWAIT: begin
if (!attach_cond) begin
// it went away before it was believed
state <= S_UNATTACHED;
gli_c <= gli_c + 16'd1;
end else if (sit_now != sit_lat) begin
// Still attached, but it is not the same attachment any more.
// The window restarts from here: the clock on "has this been
// stable" runs from the LAST change, not from the first.
ticks <= 16'd0;
sit_lat <= sit_now;
end else if (tick) begin
if (ticks == DEBOUNCE_TICKS[15:0] - 16'd1) begin
state <= S_ATTACHED;
// Taken from the DEBOUNCED situation, which by construction
// has been stable for the whole window. Note that this makes
// the sampling instant irrelevant: sit_lat and sit_now are
// equal here, so latching at entry and latching at
// acceptance would give the same answer. That equivalence is
// the point -- it is what a correct debounce buys, and
// without the restart above it would not hold.
orient_r <= sit_lat[0];
cable_r <= sit_lat[1];
debug_r <= sit_lat[2];
att_c <= att_c + 16'd1;
end else begin
ticks <= ticks + 16'd1;
end
end
end
// -------------------------------------------------------------
S_ATTACHED: begin
if (!attach_cond) begin
state <= S_DETACHWAIT;
ticks <= 16'd0;
end
end
// -------------------------------------------------------------
// A detach is debounced too, and for the same reason in reverse:
// a momentary break while the cable is wiggled must not tear down
// a power contract and a display link.
//
// `attached` stays HIGH throughout this state. A port that
// dropped it on the first missing sample would produce exactly
// the disconnect it is trying to avoid.
// -------------------------------------------------------------
S_DETACHWAIT: begin
if (attach_cond) begin
state <= S_ATTACHED; // it came back: nothing happened
end else if (tick) begin
if (ticks == DEBOUNCE_TICKS[15:0] - 16'd1) begin
state <= S_UNATTACHED;
det_c <= det_c + 16'd1;
end else begin
ticks <= ticks + 16'd1;
end
end
end
default: state <= S_UNATTACHED;
endcase
end
end
endmoduleThe debounce is on the SITUATION, not on the attachment
The comment around sit_now is long because the distinction it draws was a
design gap, and the thing that exposed it was two mutations scoring the same
number.
The first version of this module debounced attach_cond — the single bit
meaning "something is plugged in". That is the obvious reading of "the CC state
must be stable for tCCDebounce", and it is wrong, because the CC state is not
the same thing as the fact of attachment:
tick 1 CC1=Rd CC2=OPEN attach_cond = 1
tick 2 CC1=Rd CC2=OPEN attach_cond = 1
tick 3 CC1=OPEN CC2=Rd attach_cond = 1 <-- never dropped
...
tick 10 CC1=OPEN CC2=Rd attach_cond = 1 -> ATTACHED, orientation 1attach_cond held for the entire window, so a machine watching only that bit
accepts the attachment — with an orientation that had been stable for one
tick. A plug being rocked into place does exactly this.
The fix is to debounce the whole decoded situation, and to restart the window whenever any part of it changes. Three lines, and it buys something that is not obvious:
4. What The Machine Actually Does
Two scenarios, dumped cycle by cycle from the simulator. They are the same insertion, performed twice, with the plug turned over in between — which is the chapter's headline property made visible.
DEBOUNCE_TICKS is set to 3 for these two dumps so a whole reject-then-accept
cycle fits in ten cycles. The design and the suite use 10.
A plug being pushed home: one bounce rejected, then the contact believed
The same insertion, plug turned over
5. The Testbench (Verilog)
PHASE 1 DIRECTED, EXHAUSTIVE all 81 ordered pairs of CC states
PHASE 2 DIRECTED, EXHAUSTIVE the debounce threshold, every width
0..DEBOUNCE+2 for all five attach
combinations
PHASE 3 DIRECTED, EXHAUSTIVE THE SYMMETRY: all 81 pairs replayed
flipped and compared sample by sample
PHASE 4 DIRECTED a detach that changes its mind, and the
SECOND attach swept across the window
PHASE 5 DIRECTED, EXHAUSTIVE the situation changes mid-window: all 20
ordered pairs of distinct attach
situations, at three points in the window
PHASE 6 RANDOM a plug being pushed home, both lines
bouncing independentlyThree choices in the bench are worth naming before the listing.
The model is a run-length threshold, not a state machine. The design is four
states and a counter. The model counts how many consecutive ticks the decoded
situation has held and compares against DEBOUNCE — it has no state names at
all. A wrong state encoding in the design cannot produce the same mistake in the
model, which is the entire point of having one.
CC changes land strictly between ticks. CC is asynchronous to the debounce tick in real hardware, and letting a change land on the same cycle as a tick makes "how many ticks has this held for" ambiguous by one — in the bench's bookkeeping, not in the design. Keeping them apart removes the ambiguity rather than papering over it with a tolerance.
The glitch rule is stated in English before it is coded. A glitch is an attach condition that was asserted and then withdrawn before it had held long enough to be believed. Note what is absent: any clause about lasting at least one tick. The design enters its debounce state on the clock edge after the condition appears, so a condition that comes and goes between two ticks is still an abandoned attach. Adding that clause to the model cost 60 failures against a design that was right.
// =====================================================================
// Testbench for typec_cc_fsm.
//
// THE HEADLINE PROPERTY IS A SYMMETRY, AND IT IS THE REASON THE
// CONNECTOR CAN BE REVERSIBLE.
//
// Every other chapter in this module asserts that the design computes
// the right VALUE. This one asserts that it computes the SAME value
// under a transformation of its inputs: exchange CC1 and CC2 -- which is
// physically what happens when the user turns the plug over -- and
// every output must be identical except `orientation`, which must
// invert.
//
// That is a stronger statement than "it works both ways round", and it
// is checkable EXHAUSTIVELY over single transitions: 9 CC states x 9 CC
// states = 81 ordered pairs, each replayed flipped and compared sample
// by sample. A design that special-cased CC1 -- which is the natural way
// to write it, and wrong -- fails the symmetry without failing any
// single-orientation test.
//
// THE MODEL IS A RUN-LENGTH THRESHOLD, NOT A STATE MACHINE. The design
// is four states and a counter; the model counts how many consecutive
// ticks the attach condition has held and compares against DEBOUNCE.
// Same behaviour, different formulation, so a state-encoding mistake in
// one cannot be reproduced by the other.
// =====================================================================
`timescale 1ns/1ps
module tb_tc_v;
localparam integer DEBOUNCE = 10;
localparam [1:0] L_OPEN = 2'b00, L_RA = 2'b01, L_RD = 2'b10;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg tick = 1'b0;
reg [1:0] cc1 = L_OPEN, cc2 = L_OPEN;
wire attached, orientation, cable_powered, debug_accessory;
wire [15:0] n_attach, n_detach, n_glitch;
typec_cc_fsm #(.DEBOUNCE_TICKS(DEBOUNCE)) dut (
.clk(clk), .rst_n(rst_n), .tick(tick),
.cc1_st(cc1), .cc2_st(cc2),
.attached(attached), .orientation(orientation),
.cable_powered(cable_powered), .debug_accessory(debug_accessory),
.n_attach(n_attach), .n_detach(n_detach), .n_glitch(n_glitch)
);
integer errors = 0, checks = 0, steps = 0;
integer seed;
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
// 1024 bits, not 256. A `what` of [255:0] holds 32 characters, and every
// message below is longer than that -- so the one artefact a reader gets
// when a check fails would have arrived cut in half. A diagnostic that
// cannot say what went wrong is not a diagnostic.
task ck(input cond, input [1023: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
// ---- the model, formulated as a run-length threshold ----
//
// The design is a four-state machine with a tick counter. The model
// holds no state names at all: it counts consecutive ticks during which
// the attach condition has held its current value, and flips
// `m_attached` when that run reaches DEBOUNCE. A wrong state encoding
// in the design cannot produce the same mistake here.
integer m_run;
integer m_attached, m_orient, m_cable, m_debug;
integer m_att_c, m_det_c, m_gli_c;
reg m_prev_cond;
function integer f_one_rd(input [1:0] a, input [1:0] b);
begin f_one_rd = ((a == L_RD) ^ (b == L_RD)) ? 1 : 0; end
endfunction
function integer f_both_rd(input [1:0] a, input [1:0] b);
begin f_both_rd = ((a == L_RD) && (b == L_RD)) ? 1 : 0; end
endfunction
function integer f_cond(input [1:0] a, input [1:0] b);
begin f_cond = (f_one_rd(a,b) || f_both_rd(a,b)) ? 1 : 0; end
endfunction
function integer f_orient(input [1:0] a, input [1:0] b);
begin f_orient = (b == L_RD) ? 1 : 0; end
endfunction
function integer f_cable(input [1:0] a, input [1:0] b);
begin
if (f_one_rd(a,b) == 0) f_cable = 0;
else if (a == L_RD) f_cable = (b == L_RA) ? 1 : 0;
else f_cable = (a == L_RA) ? 1 : 0;
end
endfunction
// The decoded SITUATION -- the three answers bundled, which is what the
// debounce window actually applies to. Written here as an integer so the
// model can say "has anything changed" in one comparison, exactly as the
// design does, without sharing a line of code with it.
function integer f_sit(input [1:0] a, input [1:0] b);
begin
f_sit = f_both_rd(a,b) * 4 + f_cable(a,b) * 2 + f_orient(a,b);
end
endfunction
task reset_dut;
begin
rst_n = 1'b0; tick = 1'b0; cc1 = L_OPEN; cc2 = L_OPEN;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
m_run = 0; m_attached = 0; m_orient = 0; m_cable = 0; m_debug = 0;
m_att_c = 0; m_det_c = 0; m_gli_c = 0;
m_prev_cond = 1'b0;
end
endtask
// ---- change CC strictly BETWEEN ticks ----
//
// CC is asynchronous to the debounce tick in real hardware, and letting
// a change land on the same cycle as a tick makes "how many ticks has
// this held for" ambiguous by one -- in the bench's bookkeeping, not in
// the design. Keeping them apart removes the ambiguity rather than
// papering over it.
task set_cc(input [1:0] a, input [1:0] b);
integer was_cond, now_cond, was_sit, now_sit;
begin
was_cond = f_cond(cc1, cc2);
now_cond = f_cond(a, b);
was_sit = f_sit(cc1, cc2);
now_sit = f_sit(a, b);
// ---- the model's glitch rule, stated in English ----
//
// A glitch is an attach condition that was asserted and then
// WITHDRAWN before it had held long enough to be believed. Note
// there is no "and it lasted at least one tick" clause: the design
// enters its debounce state on the clock edge after the condition
// appears, so a condition that comes and goes between two ticks is
// still an abandoned attach and is still counted. Requiring a tick
// here cost 60 failures and the design was right.
if (!m_attached && was_cond && !now_cond)
m_gli_c = m_gli_c + 1;
// ---- what restarts the window ----
//
// While UNATTACHED, the window applies to the whole decoded
// situation: a sink moving from one CC line to the other leaves
// `attach_cond` true and still restarts the clock, because it is not
// the same attachment any more.
//
// While ATTACHED, only the fact of attachment matters -- the design
// holds its latched answers rather than re-deciding them, and a
// physical change of orientation without an intervening detach is
// not something a connector can do.
if (m_attached) begin
if (was_cond != now_cond) m_run = 0;
end else begin
if (was_cond != now_cond || was_sit != now_sit) m_run = 0;
end
cc1 = a; cc2 = b;
@(posedge clk); #1;
steps = steps + 1;
end
endtask
// ---- one debounce tick, with CC held ----
task do_tick;
integer cond;
begin
cond = f_cond(cc1, cc2);
tick = 1'b1;
@(posedge clk); #1;
tick = 1'b0;
// advance the model
if (cond != m_attached) begin
m_run = m_run + 1;
if (m_run == DEBOUNCE) begin
if (cond) begin
m_attached = 1;
m_orient = f_orient(cc1, cc2);
m_cable = f_cable(cc1, cc2);
m_debug = f_both_rd(cc1, cc2);
m_att_c = m_att_c + 1;
end else begin
m_attached = 0;
m_det_c = m_det_c + 1;
end
m_run = 0;
end
end else begin
m_run = 0;
end
// ---- PROPERTY 1: attach follows the model exactly ----
ck(attached === (m_attached ? 1'b1 : 1'b0),
"attached disagrees with the run-length model");
// ---- PROPERTY 2: the three latched answers are right ----
//
// Checked only while attached: outside an attachment the spec gives
// them no meaning, and asserting a value the design is free to hold
// would be asserting an implementation detail rather than a contract.
if (m_attached) begin
ck(orientation === (m_orient ? 1'b1 : 1'b0),
"orientation disagrees with which line carried Rd");
ck(cable_powered === (m_cable ? 1'b1 : 1'b0),
"cable_powered disagrees with Ra on the free line");
ck(debug_accessory === (m_debug ? 1'b1 : 1'b0),
"debug_accessory disagrees with both lines pulled to Rd");
end
// ---- PROPERTY 3: the counters agree with the model's tally ----
ck(n_attach == m_att_c[15:0], "n_attach disagrees with the model");
ck(n_detach == m_det_c[15:0], "n_detach disagrees with the model");
ck(n_glitch == m_gli_c[15:0], "n_glitch disagrees with the model");
// Sample the trace once per tick. Without this call `tr_n` stays at
// zero, the comparison loop in phase 3 has an empty range, and the
// symmetry property -- the whole point of the chapter -- is never
// evaluated while every one of its checks reports success.
rec;
steps = steps + 1;
end
endtask
task ticks_n(input integer n);
integer i;
begin for (i = 0; i < n; i = i + 1) do_tick; end
endtask
// ---- the trace, for the symmetry comparison ----
//
// Four bits per tick: attached, orientation, cable_powered,
// debug_accessory. Two runs of the same sequence -- one plain, one with
// the two CC lines exchanged -- must agree on all of it except the
// orientation bit, which must differ whenever a single-Rd sink is
// attached.
reg [3:0] tr_a [0:511];
reg [3:0] tr_b [0:511];
integer tr_n;
reg recording;
task rec;
begin
if (recording && tr_n < 512) begin
tr_a[tr_n] = {debug_accessory, cable_powered, orientation, attached};
tr_n = tr_n + 1;
end
end
endtask
// ---- exhaustive reach over ordered pairs of CC states ----
reg reach [0:80];
integer nr, ri;
integer i, j, w, g, p, q;
reg [1:0] ST [0:2];
// The five CC combinations that constitute an attach, and they have five
// DISTINCT decoded situations -- which is what makes the ordered pairs
// below a meaningful sweep rather than a list with duplicates in it.
reg [1:0] ATT1 [0:4];
reg [1:0] ATT2 [0:4];
// the glitch table: accepted (1) or not (0), per width, per combination
integer tab_w [0:12];
integer tab_acc [0:4][0:12];
integer tab_n;
integer a_idx, b_idx;
initial begin
ST[0] = L_OPEN; ST[1] = L_RA; ST[2] = L_RD;
ATT1[0] = L_OPEN; ATT2[0] = L_RD; // sink on CC2
ATT1[1] = L_RA; ATT2[1] = L_RD; // sink on CC2, powered cable
ATT1[2] = L_RD; ATT2[2] = L_OPEN; // sink on CC1
ATT1[3] = L_RD; ATT2[3] = L_RA; // sink on CC1, powered cable
ATT1[4] = L_RD; ATT2[4] = L_RD; // debug accessory
for (ri = 0; ri < 81; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd29004;
recording = 1'b0;
tr_n = 0;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every ordered pair of CC
// states, held long enough to be believed.
//
// 81 pairs. For each: settle in the `from` state, switch to the
// `to` state, hold it past the debounce window, and check the
// design against the model at every tick.
// =============================================================
for (i = 0; i < 9; i = i + 1)
for (j = 0; j < 9; j = j + 1) begin
reset_dut;
set_cc(ST[i/3], ST[i%3]);
ticks_n(DEBOUNCE + 2);
set_cc(ST[j/3], ST[j%3]);
ticks_n(DEBOUNCE + 2);
reach[i*9 + j] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- the debounce threshold.
//
// For each of the five CC combinations that constitute an attach,
// present it for every width from 0 to DEBOUNCE+2 ticks and then
// remove it. The design must accept the attach if and only if the
// width reached DEBOUNCE -- no earlier, and not never.
//
// This is the measurement the chapter reports, and it is swept
// exhaustively because the interesting values are all adjacent:
// DEBOUNCE-1 must be rejected and DEBOUNCE must be accepted, and a
// suite that tested 0 and 100 would pass with either bound wrong.
// =============================================================
tab_n = 0;
for (g = 0; g < 9; g = g + 1) begin
if (f_cond(ST[g/3], ST[g%3])) begin
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) begin
reset_dut;
set_cc(ST[g/3], ST[g%3]);
ticks_n(w);
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
// ---- PROPERTY 4: the threshold is exactly DEBOUNCE ----
ck((n_attach == 16'd1) == (w >= DEBOUNCE),
"an attach was accepted at the wrong debounce width");
// ---- PROPERTY 5: a rejected attach is COUNTED as a glitch ----
//
// Silently discarding a bouncing contact is the same behaviour
// as never seeing it, and a bring-up engineer cannot tell a
// dead cable from a bouncing one without this counter.
if (w > 0 && w < DEBOUNCE)
ck(n_glitch == 16'd1, "a rejected attach was not counted as a glitch");
if (tab_n < 5) tab_acc[tab_n][w] = (n_attach == 16'd1) ? 1 : 0;
tab_w[w] = w;
end
tab_n = tab_n + 1;
end
end
// =============================================================
// PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE SYMMETRY.
//
// Replay all 81 ordered pairs with CC1 and CC2 exchanged, and
// require the two traces to agree sample by sample on `attached`,
// `cable_powered` and `debug_accessory`, and to DISAGREE on
// `orientation` whenever a single-Rd sink is attached.
//
// Why orientation is exempt when debug_accessory is set: with Rd on
// BOTH lines there is no "other" line, so exchanging them changes
// nothing and orientation is identical rather than inverted. That is
// not a hole in the property -- it is the property, stated exactly.
// =============================================================
for (i = 0; i < 9; i = i + 1)
for (j = 0; j < 9; j = j + 1) begin
// ---- the plain run ----
reset_dut;
recording = 1'b1; tr_n = 0;
set_cc(ST[i/3], ST[i%3]); ticks_n(DEBOUNCE + 2);
set_cc(ST[j/3], ST[j%3]); ticks_n(DEBOUNCE + 2);
recording = 1'b0;
for (p = 0; p < tr_n; p = p + 1) tr_b[p] = tr_a[p];
q = tr_n;
// ---- the same sequence, plug turned over ----
reset_dut;
recording = 1'b1; tr_n = 0;
set_cc(ST[i%3], ST[i/3]); ticks_n(DEBOUNCE + 2);
set_cc(ST[j%3], ST[j/3]); ticks_n(DEBOUNCE + 2);
recording = 1'b0;
ck(tr_n == q, "the flipped run produced a different number of samples");
for (p = 0; p < q && p < tr_n; p = p + 1) begin
// ---- PROPERTY 6: attach is orientation-INDEPENDENT ----
ck(tr_a[p][0] === tr_b[p][0],
"the plug turned over changed WHETHER something was attached");
// ---- PROPERTY 7: so are the cable and debug answers ----
ck(tr_a[p][2] === tr_b[p][2],
"the plug turned over changed the powered-cable answer");
ck(tr_a[p][3] === tr_b[p][3],
"the plug turned over changed the debug-accessory answer");
// ---- PROPERTY 8: orientation INVERTS, for a single-Rd sink ----
if (tr_b[p][0] && !tr_b[p][3])
ck(tr_a[p][1] !== tr_b[p][1],
"the plug turned over did NOT change the reported orientation");
// ---- PROPERTY 9: and does NOT invert for a debug accessory ----
if (tr_b[p][0] && tr_b[p][3])
ck(tr_a[p][1] === tr_b[p][1],
"a debug accessory reported a different orientation when flipped");
end
end
// =============================================================
// PHASE 4 (DIRECTED) -- a detach that changes its mind.
//
// A momentary break while the cable is wiggled must NOT tear down
// the attachment. `attached` has to stay high across the whole
// detach-debounce window, and the attach counter must not advance
// when the condition returns -- nothing was re-attached.
// =============================================================
reset_dut;
set_cc(L_RD, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b1, "a fully debounced attach did not take");
for (w = 0; w < DEBOUNCE; w = w + 1) begin
set_cc(L_OPEN, L_OPEN);
ticks_n(w);
ck(attached === 1'b1,
"a momentary break tore down an established attachment");
set_cc(L_RD, L_OPEN);
ticks_n(2);
ck(attached === 1'b1, "the attachment did not survive the break");
ck(n_attach == 16'd1,
"a break that came back was counted as a second attach");
ck(n_detach == 16'd0, "a break that came back was counted as a detach");
end
// and now let it go for real
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b0, "a fully debounced detach did not take");
ck(n_detach == 16'd1, "the real detach was not counted");
// ---- PROPERTY 10: the SECOND attach gets the same window ----
//
// Every other phase resets the DUT before each attach, so every one
// of them is the first. That makes an entire class of bug invisible:
// a debounce counter that is not cleared when the wait state is
// re-entered still gives the first attach a full window and gives
// every one after it whatever was left over.
//
// Here the port has already attached and detached once, so the
// counter is wherever the previous attachment left it. A fresh attach
// held one tick short must still be rejected.
// Swept across the whole window rather than tested at one point: the
// stale-counter bug leaves an ARBITRARY residue behind, so which width
// exposes it depends on what the previous attachment happened to do.
// A single probe at DEBOUNCE-1 would find some residues and miss others.
for (w = 0; w <= DEBOUNCE; w = w + 1) begin
set_cc(L_RD, L_OPEN);
ticks_n(w);
ck((attached === 1'b1) == (w >= DEBOUNCE),
"the second attach used a different debounce window from the first");
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b0, "the port did not return to unattached");
end
// =============================================================
// PHASE 5 (DIRECTED, EXHAUSTIVE) -- the situation changes DURING the
// window, and the window restarts.
//
// This is the phase that separates "debounce the fact of attachment"
// from "debounce the CC state", and they are not the same thing. A
// sink moving from CC1 to CC2 part-way through the window -- a plug
// being rocked into place -- never drops `attach_cond`. A machine
// watching only `attach_cond` would accept that attachment with an
// orientation that had been stable for a single tick.
//
// Every ordered pair of distinct attach situations, at three points
// in the window: the clock must restart from the change, so
// acceptance lands DEBOUNCE ticks after it and not DEBOUNCE minus
// however long the first one was held.
// =============================================================
for (p = 0; p < 5; p = p + 1)
for (q = 0; q < 5; q = q + 1)
if (p != q) begin
for (g = 0; g < 3; g = g + 1) begin
w = (g == 0) ? 1 : (g == 1) ? 3 : DEBOUNCE - 1;
reset_dut;
set_cc(ATT1[p], ATT2[p]);
ticks_n(w);
ck(attached === 1'b0, "the first situation was accepted too early");
set_cc(ATT1[q], ATT2[q]);
// ---- PROPERTY 11: the window restarts from the CHANGE ----
ticks_n(DEBOUNCE - 1);
ck(attached === 1'b0,
"a mid-window change of CC state did not restart the debounce");
do_tick;
ck(attached === 1'b1,
"the restarted window did not complete one tick later");
// ---- PROPERTY 12: the answers are the SECOND situation's ----
//
// Not the first's. The whole reason to restart is that the
// attachment being accepted is the one that has been stable.
ck(orientation === (f_orient(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted orientation was the one that had already gone away");
ck(cable_powered === (f_cable(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted cable answer was the stale one");
ck(debug_accessory === (f_both_rd(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted debug answer was the stale one");
// ---- PROPERTY 13: no glitch was counted ----
//
// Nothing was abandoned. The attach condition held throughout; it
// was only ever a different attachment.
ck(n_glitch == 16'd0,
"a change of situation was miscounted as an abandoned attach");
end
end
// =============================================================
// PHASE 6 (RANDOM) -- a plug being pushed home.
//
// Real insertion is not one clean edge. The contacts make and break
// several times over a few milliseconds, in whatever order the
// mechanics happen to produce, and both CC lines bounce
// independently.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (w = 0; w < 400; w = w + 1) begin
a_idx = urand(0) % 3;
b_idx = urand(0) % 3;
set_cc(ST[a_idx], ST[b_idx]);
// 1..16 ticks, not 1..4. Against a ten-tick window a hold of at most
// four can NEVER complete one, so the phase produced 400 events and
// zero attaches -- it exercised the glitch path and nothing else.
// Widening the hold past the window is what lets the random phase
// reach the attached state, and therefore the detach path too.
ticks_n(1 + (urand(0) % 16));
end
`endif
nr = 0; for (ri = 0; ri < 81; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/81 errors=%0d",
steps, checks, nr, errors);
$display("[typec] attaches=%0d detaches=%0d glitches=%0d",
n_attach, n_detach, n_glitch);
$display("--- was the attach accepted, by how long it was held ---");
$write(" ticks held ");
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) $write("%3d", w);
$write("\n");
for (p = 0; p < 5; p = p + 1) begin
$write(" combo %0d ", p);
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) $write("%3d", tab_acc[p][w]);
$write("\n");
end
if (nr != 81) 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. The Measurement
The threshold is exactly the window, for every combination
Each of the five CC combinations that constitute an attach, presented for every
width from 0 to DEBOUNCE+2 ticks and then removed. 1 means the port believed
it:
--- was the attach accepted, by how long it was held ---
ticks held 0 1 2 3 4 5 6 7 8 9 10 11 12
combo 0 0 0 0 0 0 0 0 0 0 0 1 1 1
combo 1 0 0 0 0 0 0 0 0 0 0 1 1 1
combo 2 0 0 0 0 0 0 0 0 0 0 1 1 1
combo 3 0 0 0 0 0 0 0 0 0 0 1 1 1
combo 4 0 0 0 0 0 0 0 0 0 0 1 1 1
combo 0 CC2=Rd a sink on CC2
combo 1 CC1=Ra CC2=Rd a sink on CC2, powered cable
combo 2 CC1=Rd a sink on CC1
combo 3 CC1=Rd CC2=Ra a sink on CC1, powered cable
combo 4 CC1=Rd CC2=Rd a debug accessoryA step function, in the same place, five times. Nine ticks is rejected and ten is accepted, whichever line the sink is on and whether or not there is a powered cable in the way.
The sweep is exhaustive rather than sampled, and that is the point: the
interesting values are adjacent. DEBOUNCE-1 must be rejected and DEBOUNCE
must be accepted, and a suite that tested 0 and 100 would pass with either bound
off by one. An off-by-one here is the difference between rejecting insertion
bounce and enumerating in the middle of it.
The symmetry
All 81 ordered pairs of CC states, each run twice — once as given, once with CC1 and CC2 exchanged — and the two traces compared sample by sample, 24 samples per run:
attached MUST be identical
cable_powered MUST be identical
debug_accessory MUST be identical
orientation MUST INVERT -- when a single-Rd sink is attached
orientation MUST be identical -- when a DEBUG ACCESSORY is attachedThat last line is not an exemption carved out to make the property pass. With Rd on both lines there is no "other" line, so exchanging them changes nothing and the orientation is identical rather than inverted. Stating it as a separate clause rather than excluding the case is what makes the property exact: the debug-accessory rows are still checked, just against the correct expectation.
The mid-window restart
All 20 ordered pairs of distinct attach situations — the five combinations above have five distinct decoded situations, so every pair is a real change — presented at three points in the window: one tick in, three ticks in, and one tick from completion.
hold situation A for k ticks k = 1, 3, DEBOUNCE-1
switch to situation B
DEBOUNCE-1 more ticks -> still NOT attached
one more tick -> attached, with B's answers
and n_glitch is still 0 -> nothing was abandonedThe last line is the one that distinguishes this from a detach. The attach condition never dropped; it was only ever a different attachment. A machine that restarted by bouncing through its unattached state would get the timing right and the glitch count wrong, and the counter is the only thing that would notice.
Run totals
steps checks reach errors
Verilog-2005 12,564 64,920 81/81 0
SystemVerilog 12,564 64,920 81/81 0
VHDL-2008 12,777 65,760 81/81 0
DIRECTED ONLY (-D DIRECTED_ONLY / -gDIRECTED_ONLY=true)
Verilog-2005 8,918 46,746 81/81 0
SystemVerilog 8,918 46,746 81/81 0
VHDL-2008 8,918 46,746 81/81 0The directed rows agree to the check across all three languages, which is what
makes the directed mutation columns in section 11 comparable. The full rows do
not, and should not: the random phase uses $random in the two Icarus benches
and a VHDL-native generator in the third, so a different number of bounces
happen to be produced and a different number of checks fire.
7. SystemVerilog
Same hardware contract: same ports, same widths, same reset values, same cycle-by-cycle behaviour. What changes is that the three-level CC reading and the four machine states become enums, and the decoded situation becomes a packed struct rather than three bits kept in the right order by hand.
Verilog SystemVerilog
------------------------- ----------------------------------
localparam L_RD = 2'b10 typedef enum { CC_OPEN, CC_RA, CC_RD }
2'b11 is representable 2'b11 is not a value of the type
{both_rd, cable, orient} typedef struct packed { debug; cable; orient; }
sit_lat[2] is the debug bit sit_lat.debug is the debug bitThe struct earns its place at the point of use: sit_lat[0] and sit_lat[2]
are one transposition away from latching the cable answer into the orientation
register, and sit_lat.orient is not.
// =====================================================================
// typec_cc_fsm -- SystemVerilog.
//
// Same hardware contract as the Verilog file: same ports, same widths,
// same reset values, same cycle-by-cycle behaviour. What changes is that
// the three-level CC reading and the four machine states become ENUMS,
// so an illegal value is a compile error rather than a silent default,
// and the decoded situation becomes a packed struct rather than three
// bits that have to be kept in the right order by hand.
//
// EVERY CONTINUOUS ASSIGNMENT IS `logic` + `assign` ON SEPARATE LINES.
// `logic x = expr;` is a one-shot VARIABLE INITIALISER in SystemVerilog,
// evaluated once at time zero and never again, while the Verilog
// `wire x = expr;` it came from is a continuous assignment. Earlier in
// this track the mechanical translation of five such lines produced
// 29,580 phantom failures against a design that was entirely correct.
//
// typec_cc_fsm -- the Configuration Channel state machine that decides
// whether anything is plugged in, which way round it is, and how much
// current it is allowed to draw.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a Type-C port controller. There is no VBUS switch, no
// Power Delivery protocol engine, no BMC PHY, no VCONN switching and no
// alternate-mode entry. It is the CC attach machine: the part that runs
// before any of that exists, and that everything else depends on.
//
// WHY THIS IS THE MECHANISM WORTH BUILDING FOR "PHONE ON A CABLE"
// ---------------------------------------------------------------
// A phone on a USB-C cable is simultaneously a mass-storage-ish device
// (MTP), a camera (PTP), a serial console (ADB), possibly a display
// source (DisplayPort alt mode), and either a power sink or a power
// source. Which of those it is gets NEGOTIATED, and none of that
// negotiation can start until three questions are answered:
//
// 1. IS ANYTHING THERE? -- and is it stable, or is it a
// contact bouncing as the plug seats
// 2. WHICH WAY ROUND IS IT? -- the connector is reversible, so the
// same pin is TX in one orientation
// and RX in the other
// 3. HOW MUCH CURRENT? -- advertised as a resistor value
// before a single bit is exchanged
//
// All three are answered by two pins and some resistors, and the answers
// are read as VOLTAGES rather than as a protocol. That is deliberate: it
// works with a dumb cable, a dumb charger and no firmware running.
//
// THE CC LINES
// ------------
// A source (DFP) pulls both CC lines up through Rp. A sink (UFP) pulls
// its one connected CC line down through Rd. A powered cable identifies
// itself with Ra on the other line. The source reads each line as one of
// three levels:
//
// OPEN nothing pulling down -- nothing on this line
// Ra pulled down weakly -- a powered (e-marked) cable
// Rd pulled down harder -- a SINK is on this line
//
// Because the plug is reversible, the sink's Rd lands on CC1 or CC2
// depending on which way the user inserted it, and WHICH ONE IT IS is
// the orientation. There is no other way to learn it.
// =====================================================================
// ---- the three levels a CC line can read ----
//
// A 2-bit vector would let 2'b11 through, and 2'b11 is not a thing a
// comparator can report. Naming the legal values makes the illegal one
// unrepresentable instead of merely unexpected.
typedef enum logic [1:0] {
CC_OPEN = 2'b00, // nothing pulling this line down
CC_RA = 2'b01, // a powered (e-marked) cable
CC_RD = 2'b10 // a SINK is on this line
} cc_level_t;
module typec_cc_fsm #(
// How many debounce ticks a CC state must persist before it is believed.
// The specification calls this tCCDebounce and requires 100..200 ms. A
// real port ticks at 1 ms and uses 100..200 here; this module uses 10 so
// a simulation can sweep the whole window exhaustively.
parameter integer DEBOUNCE_TICKS = 10
) (
input logic clk,
input logic rst_n,
// One pulse per debounce tick. Everything time-related in this module
// is counted in ticks, never in clock cycles, so the same RTL works at
// any clock frequency.
input logic tick,
// What the comparators say about each CC line, sampled continuously.
// 2'b00 OPEN 2'b01 Ra 2'b10 Rd
input cc_level_t cc1_st,
input cc_level_t cc2_st,
// ---- the three answers ----
output logic attached,
// 1'b0 = the sink is on CC1, 1'b1 = the sink is on CC2. Meaningless
// unless `attached`, and held at its last value rather than cleared, so
// that a detach does not momentarily steer a mux the wrong way.
output logic orientation,
// A powered cable was detected alongside the sink. VCONN would be
// supplied on the OTHER line -- the one carrying Ra.
output logic cable_powered,
// Both CC lines pulled down by Rd. That is not a sink; it is a debug
// accessory, and treating it as a sink is a real interoperability bug.
output logic debug_accessory,
output logic [15:0] n_attach,
output logic [15:0] n_detach,
output logic [15:0] n_glitch // attaches that failed to debounce
);
typedef enum logic [1:0] {
S_UNATTACHED = 2'd0,
S_ATTACHWAIT = 2'd1,
S_ATTACHED = 2'd2,
S_DETACHWAIT = 2'd3
} state_t;
// ---- the decoded situation, as a struct rather than three loose bits --
//
// The Verilog file packs these into a 3-bit vector and indexes it by
// number at the point of use. That works and it is one transposition
// away from latching the cable answer into the orientation register.
typedef struct packed {
logic debug; // both lines pulled to Rd
logic cable; // Ra on the line the sink is not on
logic orient; // 0 = sink on CC1, 1 = sink on CC2
} situation_t;
state_t state;
logic [15:0] ticks;
// What is currently being debounced. See sit_now below.
situation_t sit_lat;
logic orient_r, cable_r, debug_r;
logic [15:0] att_c, det_c, gli_c;
// ---- decode the two lines ----
logic cc1_rd;
assign cc1_rd = (cc1_st == CC_RD);
logic cc2_rd;
assign cc2_rd = (cc2_st == CC_RD);
logic cc1_ra;
assign cc1_ra = (cc1_st == CC_RA);
logic cc2_ra;
assign cc2_ra = (cc2_st == CC_RA);
// EXACTLY ONE line pulled down by Rd is a sink. The exclusive-or is the
// whole rule, and it is what makes the connector reversible: the machine
// does not care which line it is, only that precisely one of them is.
logic one_rd;
assign one_rd = cc1_rd ^ cc2_rd;
// BOTH lines pulled down by Rd is a debug accessory, not a sink. A port
// that treats it as a sink will drive VBUS into a debug jig.
logic both_rd;
assign both_rd = cc1_rd & cc2_rd;
logic attach_cond;
assign attach_cond = one_rd | both_rd;
// Which line the sink is on. Only meaningful when exactly one Rd is
// present, which is exactly when it is sampled.
logic orient_now;
assign orient_now = cc2_rd;
// Ra on the line the sink is NOT on. With both_rd there is no free line,
// so there is no powered cable to find.
logic cable_now;
assign cable_now = one_rd ? (cc1_rd ? cc2_ra : cc1_ra) : 1'b0;
// ---- the SITUATION, which is what actually gets debounced ----
//
// The specification debounces the CC STATE, not the fact that something
// is attached. Those are different, and the difference is a real bug:
// if Rd moves from CC1 to CC2 part-way through the window -- which is
// exactly what a plug being rocked into place does -- then attach_cond
// never drops, and a machine watching only attach_cond would accept the
// attachment with an orientation that had been stable for one tick.
//
// Bundling the three decoded answers into one value makes "has anything
// about this changed" a single comparison, and makes it impossible to
// debounce one answer and forget another.
situation_t sit_now;
// Assigned as a vector rather than as an assignment pattern
// ('{debug: ..., cable: ...}), which Icarus rejects in a continuous
// assignment. The field ORDER in the typedef is therefore load-bearing
// here -- which is exactly the hazard the struct was meant to remove, so
// the fields are read back by NAME everywhere else in the module and
// this is the one place the order matters.
assign sit_now = {both_rd, cable_now, orient_now};
assign attached = (state == S_ATTACHED) || (state == S_DETACHWAIT);
assign orientation = orient_r;
assign cable_powered = cable_r;
assign debug_accessory = debug_r;
assign n_attach = att_c;
assign n_detach = det_c;
assign n_glitch = gli_c;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_UNATTACHED;
ticks <= 16'd0;
sit_lat <= '0;
orient_r <= 1'b0;
cable_r <= 1'b0;
debug_r <= 1'b0;
att_c <= 16'd0;
det_c <= 16'd0;
gli_c <= 16'd0;
end else begin
case (state)
// -------------------------------------------------------------
S_UNATTACHED: begin
if (attach_cond) begin
state <= S_ATTACHWAIT;
ticks <= 16'd0;
sit_lat <= sit_now;
end
end
// -------------------------------------------------------------
// The condition has to SURVIVE tCCDebounce. This is not
// belt-and-braces: a plug being pushed home makes and breaks
// contact several times over a few milliseconds, and a port that
// believed the first edge would enumerate, drop, and enumerate
// again -- which is the "my phone keeps reconnecting" bug.
// -------------------------------------------------------------
S_ATTACHWAIT: begin
if (!attach_cond) begin
// it went away before it was believed
state <= S_UNATTACHED;
gli_c <= gli_c + 16'd1;
end else if (sit_now != sit_lat) begin
// Still attached, but it is not the same attachment any more.
// The window restarts from here: the clock on "has this been
// stable" runs from the LAST change, not from the first.
ticks <= 16'd0;
sit_lat <= sit_now;
end else if (tick) begin
if (ticks == DEBOUNCE_TICKS[15:0] - 16'd1) begin
state <= S_ATTACHED;
// Taken from the DEBOUNCED situation, which by construction
// has been stable for the whole window. Note that this makes
// the sampling instant irrelevant: sit_lat and sit_now are
// equal here, so latching at entry and latching at
// acceptance would give the same answer. That equivalence is
// the point -- it is what a correct debounce buys, and
// without the restart above it would not hold.
orient_r <= sit_lat.orient;
cable_r <= sit_lat.cable;
debug_r <= sit_lat.debug;
att_c <= att_c + 16'd1;
end else begin
ticks <= ticks + 16'd1;
end
end
end
// -------------------------------------------------------------
S_ATTACHED: begin
if (!attach_cond) begin
state <= S_DETACHWAIT;
ticks <= 16'd0;
end
end
// -------------------------------------------------------------
// A detach is debounced too, and for the same reason in reverse:
// a momentary break while the cable is wiggled must not tear down
// a power contract and a display link.
//
// `attached` stays HIGH throughout this state. A port that
// dropped it on the first missing sample would produce exactly
// the disconnect it is trying to avoid.
// -------------------------------------------------------------
S_DETACHWAIT: begin
if (attach_cond) begin
state <= S_ATTACHED; // it came back: nothing happened
end else if (tick) begin
if (ticks == DEBOUNCE_TICKS[15:0] - 16'd1) begin
state <= S_UNATTACHED;
det_c <= det_c + 16'd1;
end else begin
ticks <= ticks + 16'd1;
end
end
end
default: state <= S_UNATTACHED;
endcase
end
end
endmoduleThe SystemVerilog testbench
Same seed and same phase order as the Verilog bench, deliberately: Icarus seeds
$random identically for both, so the two drive identical stimulus and any
difference between their mutation columns is a difference between the two
designs. The independent-stimulus role belongs to VHDL.
// =====================================================================
// Testbench for typec_cc_fsm. -- 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 HEADLINE PROPERTY IS A SYMMETRY, AND IT IS THE REASON THE
// CONNECTOR CAN BE REVERSIBLE.
//
// Every other chapter in this module asserts that the design computes
// the right VALUE. This one asserts that it computes the SAME value
// under a transformation of its inputs: exchange CC1 and CC2 -- which is
// physically what happens when the user turns the plug over -- and
// every output must be identical except `orientation`, which must
// invert.
//
// That is a stronger statement than "it works both ways round", and it
// is checkable EXHAUSTIVELY over single transitions: 9 CC states x 9 CC
// states = 81 ordered pairs, each replayed flipped and compared sample
// by sample. A design that special-cased CC1 -- which is the natural way
// to write it, and wrong -- fails the symmetry without failing any
// single-orientation test.
//
// THE MODEL IS A RUN-LENGTH THRESHOLD, NOT A STATE MACHINE. The design
// is four states and a counter; the model counts how many consecutive
// ticks the attach condition has held and compares against DEBOUNCE.
// Same behaviour, different formulation, so a state-encoding mistake in
// one cannot be reproduced by the other.
// =====================================================================
`timescale 1ns/1ps
module tb_tc_sv;
localparam integer DEBOUNCE = 10;
localparam [1:0] L_OPEN = 2'b00, L_RA = 2'b01, L_RD = 2'b10;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
logic tick = 1'b0;
logic [1:0] cc1 = L_OPEN, cc2 = L_OPEN;
logic attached, orientation, cable_powered, debug_accessory;
logic [15:0] n_attach, n_detach, n_glitch;
typec_cc_fsm #(.DEBOUNCE_TICKS(DEBOUNCE)) dut (
.clk(clk), .rst_n(rst_n), .tick(tick),
.cc1_st(cc1), .cc2_st(cc2),
.attached(attached), .orientation(orientation),
.cable_powered(cable_powered), .debug_accessory(debug_accessory),
.n_attach(n_attach), .n_detach(n_detach), .n_glitch(n_glitch)
);
int errors = 0, checks = 0, steps = 0;
int seed;
function automatic logic [31:0] urand();
return $random(seed) & 32'h3FFF_FFFF;
endfunction
// 1024 bits, not 256. A `what` of [255:0] holds 32 characters, and every
// message below is longer than that -- so the one artefact a reader gets
// when a check fails would have arrived cut in half. A diagnostic that
// cannot say what went wrong is not a diagnostic.
task automatic ck(input cond, input [1023:0] 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
// ---- the model, formulated as a run-length threshold ----
//
// The design is a four-state machine with a tick counter. The model
// holds no state names at all: it counts consecutive ticks during which
// the attach condition has held its current value, and flips
// `m_attached` when that run reaches DEBOUNCE. A wrong state encoding
// in the design cannot produce the same mistake here.
int m_run;
int m_attached, m_orient, m_cable, m_debug;
int m_att_c, m_det_c, m_gli_c;
logic m_prev_cond;
function automatic integer f_one_rd(input [1:0] a, input [1:0] b);
begin f_one_rd = ((a == L_RD) ^ (b == L_RD)) ? 1 : 0; end
endfunction
function automatic integer f_both_rd(input [1:0] a, input [1:0] b);
begin f_both_rd = ((a == L_RD) && (b == L_RD)) ? 1 : 0; end
endfunction
function automatic integer f_cond(input [1:0] a, input [1:0] b);
begin f_cond = (f_one_rd(a,b) || f_both_rd(a,b)) ? 1 : 0; end
endfunction
function automatic integer f_orient(input [1:0] a, input [1:0] b);
begin f_orient = (b == L_RD) ? 1 : 0; end
endfunction
function automatic integer f_cable(input [1:0] a, input [1:0] b);
begin
if (f_one_rd(a,b) == 0) f_cable = 0;
else if (a == L_RD) f_cable = (b == L_RA) ? 1 : 0;
else f_cable = (a == L_RA) ? 1 : 0;
end
endfunction
// The decoded SITUATION -- the three answers bundled, which is what the
// debounce window actually applies to. Written here as an integer so the
// model can say "has anything changed" in one comparison, exactly as the
// design does, without sharing a line of code with it.
function automatic integer f_sit(input [1:0] a, input [1:0] b);
begin
f_sit = f_both_rd(a,b) * 4 + f_cable(a,b) * 2 + f_orient(a,b);
end
endfunction
task automatic reset_dut;
begin
rst_n = 1'b0; tick = 1'b0; cc1 = L_OPEN; cc2 = L_OPEN;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
m_run = 0; m_attached = 0; m_orient = 0; m_cable = 0; m_debug = 0;
m_att_c = 0; m_det_c = 0; m_gli_c = 0;
m_prev_cond = 1'b0;
end
endtask
// ---- change CC strictly BETWEEN ticks ----
//
// CC is asynchronous to the debounce tick in real hardware, and letting
// a change land on the same cycle as a tick makes "how many ticks has
// this held for" ambiguous by one -- in the bench's bookkeeping, not in
// the design. Keeping them apart removes the ambiguity rather than
// papering over it.
task automatic set_cc(input [1:0] a, input [1:0] b);
int was_cond, now_cond, was_sit, now_sit;
begin
was_cond = f_cond(cc1, cc2);
now_cond = f_cond(a, b);
was_sit = f_sit(cc1, cc2);
now_sit = f_sit(a, b);
// ---- the model's glitch rule, stated in English ----
//
// A glitch is an attach condition that was asserted and then
// WITHDRAWN before it had held long enough to be believed. Note
// there is no "and it lasted at least one tick" clause: the design
// enters its debounce state on the clock edge after the condition
// appears, so a condition that comes and goes between two ticks is
// still an abandoned attach and is still counted. Requiring a tick
// here cost 60 failures and the design was right.
if (!m_attached && was_cond && !now_cond)
m_gli_c = m_gli_c + 1;
// ---- what restarts the window ----
//
// While UNATTACHED, the window applies to the whole decoded
// situation: a sink moving from one CC line to the other leaves
// `attach_cond` true and still restarts the clock, because it is not
// the same attachment any more.
//
// While ATTACHED, only the fact of attachment matters -- the design
// holds its latched answers rather than re-deciding them, and a
// physical change of orientation without an intervening detach is
// not something a connector can do.
if (m_attached) begin
if (was_cond != now_cond) m_run = 0;
end else begin
if (was_cond != now_cond || was_sit != now_sit) m_run = 0;
end
cc1 = a; cc2 = b;
@(posedge clk); #1;
steps = steps + 1;
end
endtask
// ---- one debounce tick, with CC held ----
task automatic do_tick;
int cond;
begin
cond = f_cond(cc1, cc2);
tick = 1'b1;
@(posedge clk); #1;
tick = 1'b0;
// advance the model
if (cond != m_attached) begin
m_run = m_run + 1;
if (m_run == DEBOUNCE) begin
if (cond) begin
m_attached = 1;
m_orient = f_orient(cc1, cc2);
m_cable = f_cable(cc1, cc2);
m_debug = f_both_rd(cc1, cc2);
m_att_c = m_att_c + 1;
end else begin
m_attached = 0;
m_det_c = m_det_c + 1;
end
m_run = 0;
end
end else begin
m_run = 0;
end
// ---- PROPERTY 1: attach follows the model exactly ----
ck(attached === (m_attached ? 1'b1 : 1'b0),
"attached disagrees with the run-length model");
// ---- PROPERTY 2: the three latched answers are right ----
//
// Checked only while attached: outside an attachment the spec gives
// them no meaning, and asserting a value the design is free to hold
// would be asserting an implementation detail rather than a contract.
if (m_attached) begin
ck(orientation === (m_orient ? 1'b1 : 1'b0),
"orientation disagrees with which line carried Rd");
ck(cable_powered === (m_cable ? 1'b1 : 1'b0),
"cable_powered disagrees with Ra on the free line");
ck(debug_accessory === (m_debug ? 1'b1 : 1'b0),
"debug_accessory disagrees with both lines pulled to Rd");
end
// ---- PROPERTY 3: the counters agree with the model's tally ----
ck(n_attach == m_att_c[15:0], "n_attach disagrees with the model");
ck(n_detach == m_det_c[15:0], "n_detach disagrees with the model");
ck(n_glitch == m_gli_c[15:0], "n_glitch disagrees with the model");
// Sample the trace once per tick. Without this call `tr_n` stays at
// zero, the comparison loop in phase 3 has an empty range, and the
// symmetry property -- the whole point of the chapter -- is never
// evaluated while every one of its checks reports success.
rec;
steps = steps + 1;
end
endtask
task automatic ticks_n(input integer n);
int i;
begin for (i = 0; i < n; i = i + 1) do_tick; end
endtask
// ---- the trace, for the symmetry comparison ----
//
// Four bits per tick: attached, orientation, cable_powered,
// debug_accessory. Two runs of the same sequence -- one plain, one with
// the two CC lines exchanged -- must agree on all of it except the
// orientation bit, which must differ whenever a single-Rd sink is
// attached.
logic [3:0] tr_a [0:511];
logic [3:0] tr_b [0:511];
int tr_n;
logic recording;
task automatic rec;
begin
if (recording && tr_n < 512) begin
tr_a[tr_n] = {debug_accessory, cable_powered, orientation, attached};
tr_n = tr_n + 1;
end
end
endtask
// ---- exhaustive reach over ordered pairs of CC states ----
logic reach [0:80];
int nr, ri;
int i, j, w, g, p, q;
logic [1:0] ST [0:2];
// The five CC combinations that constitute an attach, and they have five
// DISTINCT decoded situations -- which is what makes the ordered pairs
// below a meaningful sweep rather than a list with duplicates in it.
logic [1:0] ATT1 [0:4];
logic [1:0] ATT2 [0:4];
// the glitch table: accepted (1) or not (0), per width, per combination
int tab_w [0:12];
integer tab_acc [0:4][0:12];
int tab_n;
int a_idx, b_idx;
initial begin
ST[0] = L_OPEN; ST[1] = L_RA; ST[2] = L_RD;
ATT1[0] = L_OPEN; ATT2[0] = L_RD; // sink on CC2
ATT1[1] = L_RA; ATT2[1] = L_RD; // sink on CC2, powered cable
ATT1[2] = L_RD; ATT2[2] = L_OPEN; // sink on CC1
ATT1[3] = L_RD; ATT2[3] = L_RA; // sink on CC1, powered cable
ATT1[4] = L_RD; ATT2[4] = L_RD; // debug accessory
for (ri = 0; ri < 81; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd29004;
recording = 1'b0;
tr_n = 0;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every ordered pair of CC
// states, held long enough to be believed.
//
// 81 pairs. For each: settle in the `from` state, switch to the
// `to` state, hold it past the debounce window, and check the
// design against the model at every tick.
// =============================================================
for (i = 0; i < 9; i = i + 1)
for (j = 0; j < 9; j = j + 1) begin
reset_dut;
set_cc(ST[i/3], ST[i%3]);
ticks_n(DEBOUNCE + 2);
set_cc(ST[j/3], ST[j%3]);
ticks_n(DEBOUNCE + 2);
reach[i*9 + j] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- the debounce threshold.
//
// For each of the five CC combinations that constitute an attach,
// present it for every width from 0 to DEBOUNCE+2 ticks and then
// remove it. The design must accept the attach if and only if the
// width reached DEBOUNCE -- no earlier, and not never.
//
// This is the measurement the chapter reports, and it is swept
// exhaustively because the interesting values are all adjacent:
// DEBOUNCE-1 must be rejected and DEBOUNCE must be accepted, and a
// suite that tested 0 and 100 would pass with either bound wrong.
// =============================================================
tab_n = 0;
for (g = 0; g < 9; g = g + 1) begin
if (f_cond(ST[g/3], ST[g%3])) begin
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) begin
reset_dut;
set_cc(ST[g/3], ST[g%3]);
ticks_n(w);
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
// ---- PROPERTY 4: the threshold is exactly DEBOUNCE ----
ck((n_attach == 16'd1) == (w >= DEBOUNCE),
"an attach was accepted at the wrong debounce width");
// ---- PROPERTY 5: a rejected attach is COUNTED as a glitch ----
//
// Silently discarding a bouncing contact is the same behaviour
// as never seeing it, and a bring-up engineer cannot tell a
// dead cable from a bouncing one without this counter.
if (w > 0 && w < DEBOUNCE)
ck(n_glitch == 16'd1, "a rejected attach was not counted as a glitch");
if (tab_n < 5) tab_acc[tab_n][w] = (n_attach == 16'd1) ? 1 : 0;
tab_w[w] = w;
end
tab_n = tab_n + 1;
end
end
// =============================================================
// PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE SYMMETRY.
//
// Replay all 81 ordered pairs with CC1 and CC2 exchanged, and
// require the two traces to agree sample by sample on `attached`,
// `cable_powered` and `debug_accessory`, and to DISAGREE on
// `orientation` whenever a single-Rd sink is attached.
//
// Why orientation is exempt when debug_accessory is set: with Rd on
// BOTH lines there is no "other" line, so exchanging them changes
// nothing and orientation is identical rather than inverted. That is
// not a hole in the property -- it is the property, stated exactly.
// =============================================================
for (i = 0; i < 9; i = i + 1)
for (j = 0; j < 9; j = j + 1) begin
// ---- the plain run ----
reset_dut;
recording = 1'b1; tr_n = 0;
set_cc(ST[i/3], ST[i%3]); ticks_n(DEBOUNCE + 2);
set_cc(ST[j/3], ST[j%3]); ticks_n(DEBOUNCE + 2);
recording = 1'b0;
for (p = 0; p < tr_n; p = p + 1) tr_b[p] = tr_a[p];
q = tr_n;
// ---- the same sequence, plug turned over ----
reset_dut;
recording = 1'b1; tr_n = 0;
set_cc(ST[i%3], ST[i/3]); ticks_n(DEBOUNCE + 2);
set_cc(ST[j%3], ST[j/3]); ticks_n(DEBOUNCE + 2);
recording = 1'b0;
ck(tr_n == q, "the flipped run produced a different number of samples");
for (p = 0; p < q && p < tr_n; p = p + 1) begin
// ---- PROPERTY 6: attach is orientation-INDEPENDENT ----
ck(tr_a[p][0] === tr_b[p][0],
"the plug turned over changed WHETHER something was attached");
// ---- PROPERTY 7: so are the cable and debug answers ----
ck(tr_a[p][2] === tr_b[p][2],
"the plug turned over changed the powered-cable answer");
ck(tr_a[p][3] === tr_b[p][3],
"the plug turned over changed the debug-accessory answer");
// ---- PROPERTY 8: orientation INVERTS, for a single-Rd sink ----
if (tr_b[p][0] && !tr_b[p][3])
ck(tr_a[p][1] !== tr_b[p][1],
"the plug turned over did NOT change the reported orientation");
// ---- PROPERTY 9: and does NOT invert for a debug accessory ----
if (tr_b[p][0] && tr_b[p][3])
ck(tr_a[p][1] === tr_b[p][1],
"a debug accessory reported a different orientation when flipped");
end
end
// =============================================================
// PHASE 4 (DIRECTED) -- a detach that changes its mind.
//
// A momentary break while the cable is wiggled must NOT tear down
// the attachment. `attached` has to stay high across the whole
// detach-debounce window, and the attach counter must not advance
// when the condition returns -- nothing was re-attached.
// =============================================================
reset_dut;
set_cc(L_RD, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b1, "a fully debounced attach did not take");
for (w = 0; w < DEBOUNCE; w = w + 1) begin
set_cc(L_OPEN, L_OPEN);
ticks_n(w);
ck(attached === 1'b1,
"a momentary break tore down an established attachment");
set_cc(L_RD, L_OPEN);
ticks_n(2);
ck(attached === 1'b1, "the attachment did not survive the break");
ck(n_attach == 16'd1,
"a break that came back was counted as a second attach");
ck(n_detach == 16'd0, "a break that came back was counted as a detach");
end
// and now let it go for real
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b0, "a fully debounced detach did not take");
ck(n_detach == 16'd1, "the real detach was not counted");
// ---- PROPERTY 10: the SECOND attach gets the same window ----
//
// Every other phase resets the DUT before each attach, so every one
// of them is the first. That makes an entire class of bug invisible:
// a debounce counter that is not cleared when the wait state is
// re-entered still gives the first attach a full window and gives
// every one after it whatever was left over.
//
// Here the port has already attached and detached once, so the
// counter is wherever the previous attachment left it. A fresh attach
// held one tick short must still be rejected.
// Swept across the whole window rather than tested at one point: the
// stale-counter bug leaves an ARBITRARY residue behind, so which width
// exposes it depends on what the previous attachment happened to do.
// A single probe at DEBOUNCE-1 would find some residues and miss others.
for (w = 0; w <= DEBOUNCE; w = w + 1) begin
set_cc(L_RD, L_OPEN);
ticks_n(w);
ck((attached === 1'b1) == (w >= DEBOUNCE),
"the second attach used a different debounce window from the first");
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached === 1'b0, "the port did not return to unattached");
end
// =============================================================
// PHASE 5 (DIRECTED, EXHAUSTIVE) -- the situation changes DURING the
// window, and the window restarts.
//
// This is the phase that separates "debounce the fact of attachment"
// from "debounce the CC state", and they are not the same thing. A
// sink moving from CC1 to CC2 part-way through the window -- a plug
// being rocked into place -- never drops `attach_cond`. A machine
// watching only `attach_cond` would accept that attachment with an
// orientation that had been stable for a single tick.
//
// Every ordered pair of distinct attach situations, at three points
// in the window: the clock must restart from the change, so
// acceptance lands DEBOUNCE ticks after it and not DEBOUNCE minus
// however long the first one was held.
// =============================================================
for (p = 0; p < 5; p = p + 1)
for (q = 0; q < 5; q = q + 1)
if (p != q) begin
for (g = 0; g < 3; g = g + 1) begin
w = (g == 0) ? 1 : (g == 1) ? 3 : DEBOUNCE - 1;
reset_dut;
set_cc(ATT1[p], ATT2[p]);
ticks_n(w);
ck(attached === 1'b0, "the first situation was accepted too early");
set_cc(ATT1[q], ATT2[q]);
// ---- PROPERTY 11: the window restarts from the CHANGE ----
ticks_n(DEBOUNCE - 1);
ck(attached === 1'b0,
"a mid-window change of CC state did not restart the debounce");
do_tick;
ck(attached === 1'b1,
"the restarted window did not complete one tick later");
// ---- PROPERTY 12: the answers are the SECOND situation's ----
//
// Not the first's. The whole reason to restart is that the
// attachment being accepted is the one that has been stable.
ck(orientation === (f_orient(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted orientation was the one that had already gone away");
ck(cable_powered === (f_cable(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted cable answer was the stale one");
ck(debug_accessory === (f_both_rd(ATT1[q], ATT2[q]) ? 1'b1 : 1'b0),
"the accepted debug answer was the stale one");
// ---- PROPERTY 13: no glitch was counted ----
//
// Nothing was abandoned. The attach condition held throughout; it
// was only ever a different attachment.
ck(n_glitch == 16'd0,
"a change of situation was miscounted as an abandoned attach");
end
end
// =============================================================
// PHASE 6 (RANDOM) -- a plug being pushed home.
//
// Real insertion is not one clean edge. The contacts make and break
// several times over a few milliseconds, in whatever order the
// mechanics happen to produce, and both CC lines bounce
// independently.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (w = 0; w < 400; w = w + 1) begin
a_idx = urand() % 3;
b_idx = urand() % 3;
set_cc(ST[a_idx], ST[b_idx]);
// 1..16 ticks, not 1..4. Against a ten-tick window a hold of at most
// four can NEVER complete one, so the phase produced 400 events and
// zero attaches -- it exercised the glitch path and nothing else.
// Widening the hold past the window is what lets the random phase
// reach the attached state, and therefore the detach path too.
ticks_n(1 + (urand() % 16));
end
`endif
nr = 0; for (ri = 0; ri < 81; ri = ri + 1) if (reach[ri]) nr = nr + 1;
$display("steps=%0d checks=%0d reach=%0d/81 errors=%0d",
steps, checks, nr, errors);
$display("[typec] attaches=%0d detaches=%0d glitches=%0d",
n_attach, n_detach, n_glitch);
$display("--- was the attach accepted, by how long it was held ---");
$write(" ticks held ");
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) $write("%3d", w);
$write("\n");
for (p = 0; p < 5; p = p + 1) begin
$write(" combo %0d ", p);
for (w = 0; w <= DEBOUNCE + 2; w = w + 1) $write("%3d", tab_acc[p][w]);
$write("\n");
end
if (nr != 81) 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
endmodule8. VHDL-2008
The third implementation, and the independent one: the two Icarus benches share their stimulus, so a defect they both have cannot be found by comparing them to each other.
VHDL's specific contribution here is the state type. An enumerated state in
VHDL has no numeric encoding at all unless one is asked for, so there is no
illegal value to fall in from and no default arm to fall into:
Verilog reg [1:0] state; four states, and 2'b11 is one of
them whether you want it or not.
The `default:` arm is load-bearing.
SystemVerilog typedef enum ... better: the enum names the legal
values, but the underlying vector
can still hold others
VHDL type state_t is (...) there is no fifth value to have.
A `when others` arm would be
unreachable code, and the compiler
says so.-- =====================================================================
-- typec_cc_fsm -- VHDL-2008.
--
-- Same hardware contract as the Verilog and SystemVerilog files: same
-- ports, same widths, same reset values, same cycle-by-cycle behaviour.
--
-- THIS IS THE INDEPENDENT IMPLEMENTATION. The two Icarus benches share
-- their stimulus, because Icarus seeds $random identically for Verilog
-- and SystemVerilog; the VHDL bench derives its own. A defect that both
-- Icarus benches share cannot be found by comparing them to each other.
--
-- VHDL's contribution here is the state type. An enumerated state in
-- VHDL has no numeric encoding at all unless one is asked for, so there
-- is no `default:` arm to fall into and no illegal value to fall in from
-- -- the synthesiser picks an encoding and the source cannot express a
-- fifth state. The Verilog file needs a `default` arm to be safe; this
-- one cannot need it.
--
-- typec_cc_fsm -- the Configuration Channel state machine that decides
-- whether anything is plugged in, which way round it is, and how much
-- current it is allowed to draw.
--
-- CLASSIFICATION: simplified synthesisable teaching RTL.
-- This is NOT a Type-C port controller. There is no VBUS switch, no
-- Power Delivery protocol engine, no BMC PHY, no VCONN switching and no
-- alternate-mode entry. It is the CC attach machine: the part that runs
-- before any of that exists, and that everything else depends on.
--
-- A source pulls both CC lines up through Rp; a sink pulls its one
-- connected line down through Rd; a powered cable identifies itself with
-- Ra on the other. Because the plug is reversible, WHICH line carries
-- the Rd is the orientation, and there is no other way to learn it.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity typec_cc_fsm is
generic (
-- The specification calls this tCCDebounce and requires 100..200 ms.
-- A real port ticks at 1 ms and uses 100..200 here; this module uses
-- 10 so a simulation can sweep the whole window exhaustively.
DEBOUNCE_TICKS : integer := 10
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- One pulse per debounce tick. Everything time-related here is
-- counted in ticks, never in clock cycles, so the same RTL works at
-- any clock frequency.
tick : in std_logic;
-- "00" OPEN "01" Ra "10" Rd
cc1_st : in std_logic_vector(1 downto 0);
cc2_st : in std_logic_vector(1 downto 0);
attached : out std_logic;
-- '0' = the sink is on CC1, '1' = on CC2. Meaningless unless
-- `attached`, and HELD rather than cleared on detach so that nothing
-- downstream is momentarily steered the wrong way.
orientation : out std_logic;
-- A powered cable alongside the sink. VCONN would be supplied on the
-- OTHER line -- the one carrying Ra.
cable_powered : out std_logic;
-- Both lines pulled down by Rd. That is not a sink; it is a debug
-- accessory, and driving VBUS into one is a real interop failure.
debug_accessory : out std_logic;
n_attach : out unsigned(15 downto 0);
n_detach : out unsigned(15 downto 0);
n_glitch : out unsigned(15 downto 0)
);
end entity;
architecture rtl of typec_cc_fsm is
constant L_OPEN : std_logic_vector(1 downto 0) := "00";
constant L_RA : std_logic_vector(1 downto 0) := "01";
constant L_RD : std_logic_vector(1 downto 0) := "10";
-- No numeric encoding, so no illegal state exists to be defaulted from.
type state_t is (S_UNATTACHED, S_ATTACHWAIT, S_ATTACHED, S_DETACHWAIT);
signal state : state_t;
signal ticks : unsigned(15 downto 0);
-- What is currently being debounced. See sit_now below.
signal sit_lat : std_logic_vector(2 downto 0);
signal orient_r : std_logic;
signal cable_r : std_logic;
signal debug_r : std_logic;
signal att_c : unsigned(15 downto 0);
signal det_c : unsigned(15 downto 0);
signal gli_c : unsigned(15 downto 0);
signal cc1_rd, cc2_rd, cc1_ra, cc2_ra : std_logic;
signal one_rd, both_rd, attach_cond : std_logic;
signal orient_now, cable_now : std_logic;
signal sit_now : std_logic_vector(2 downto 0);
begin
cc1_rd <= '1' when cc1_st = L_RD else '0';
cc2_rd <= '1' when cc2_st = L_RD else '0';
cc1_ra <= '1' when cc1_st = L_RA else '0';
cc2_ra <= '1' when cc2_st = L_RA else '0';
-- EXACTLY ONE line pulled down by Rd is a sink. The exclusive-or is the
-- whole rule, and it is what makes the connector reversible: the machine
-- does not care which line it is, only that precisely one of them is.
one_rd <= cc1_rd xor cc2_rd;
-- BOTH lines pulled down by Rd is a debug accessory, not a sink.
both_rd <= cc1_rd and cc2_rd;
attach_cond <= one_rd or both_rd;
orient_now <= cc2_rd;
-- Ra on the line the sink is NOT on. With both_rd there is no free line,
-- so there is no powered cable to find.
cable_now <= cc2_ra when (one_rd = '1' and cc1_rd = '1') else
cc1_ra when (one_rd = '1') else
'0';
-- ---- the SITUATION, which is what actually gets debounced ----
--
-- The specification debounces the CC STATE, not the fact that something
-- is attached. Those are different, and the difference is a real bug: if
-- Rd moves from CC1 to CC2 part-way through the window -- which is what a
-- plug being rocked into place does -- then attach_cond never drops, and
-- a machine watching only attach_cond would accept the attachment with
-- an orientation that had been stable for a single tick.
sit_now <= both_rd & cable_now & orient_now;
attached <= '1' when (state = S_ATTACHED or state = S_DETACHWAIT) else '0';
orientation <= orient_r;
cable_powered <= cable_r;
debug_accessory <= debug_r;
n_attach <= att_c;
n_detach <= det_c;
n_glitch <= gli_c;
process (clk, rst_n)
begin
if rst_n = '0' then
state <= S_UNATTACHED;
ticks <= (others => '0');
sit_lat <= (others => '0');
orient_r <= '0';
cable_r <= '0';
debug_r <= '0';
att_c <= (others => '0');
det_c <= (others => '0');
gli_c <= (others => '0');
elsif rising_edge(clk) then
case state is
when S_UNATTACHED =>
if attach_cond = '1' then
state <= S_ATTACHWAIT;
ticks <= (others => '0');
sit_lat <= sit_now;
end if;
-- The condition has to SURVIVE tCCDebounce. This is not
-- belt-and-braces: a plug being pushed home makes and breaks
-- contact several times over a few milliseconds, and a port that
-- believed the first edge would enumerate, drop, and enumerate
-- again -- which is the "my phone keeps reconnecting" bug.
when S_ATTACHWAIT =>
if attach_cond = '0' then
-- it went away before it was believed
state <= S_UNATTACHED;
gli_c <= gli_c + 1;
elsif sit_now /= sit_lat then
-- Still attached, but it is not the same attachment any more.
-- The window restarts from here: the clock on "has this been
-- stable" runs from the LAST change, not from the first.
ticks <= (others => '0');
sit_lat <= sit_now;
elsif tick = '1' then
if ticks = to_unsigned(DEBOUNCE_TICKS - 1, 16) then
state <= S_ATTACHED;
-- Taken from the DEBOUNCED situation, which by construction
-- has been stable for the whole window. That makes the
-- sampling instant irrelevant -- which is what a correct
-- debounce buys, and without the restart above it would not
-- hold.
orient_r <= sit_lat(0);
cable_r <= sit_lat(1);
debug_r <= sit_lat(2);
att_c <= att_c + 1;
else
ticks <= ticks + 1;
end if;
end if;
when S_ATTACHED =>
if attach_cond = '0' then
state <= S_DETACHWAIT;
ticks <= (others => '0');
end if;
-- A detach is debounced too, and for the same reason in reverse: a
-- momentary break while the cable is wiggled must not tear down a
-- power contract and a display link. `attached` stays HIGH
-- throughout this state -- a port that dropped it on the first
-- missing sample would produce exactly the disconnect it is trying
-- to avoid.
when S_DETACHWAIT =>
if attach_cond = '1' then
state <= S_ATTACHED; -- it came back: nothing happened
elsif tick = '1' then
if ticks = to_unsigned(DEBOUNCE_TICKS - 1, 16) then
state <= S_UNATTACHED;
det_c <= det_c + 1;
else
ticks <= ticks + 1;
end if;
end if;
end case;
end if;
end process;
end architecture;The VHDL testbench
The directed phases are structurally identical to the other two benches, so the directed mutation columns must agree exactly. The random phase uses a VHDL-native generator.
-- =====================================================================
-- Testbench for typec_cc_fsm -- VHDL-2008.
--
-- THE HEADLINE PROPERTY IS A SYMMETRY, AND IT IS THE REASON THE
-- CONNECTOR CAN BE REVERSIBLE.
--
-- Every other chapter in this module asserts that the design computes
-- the right VALUE. This one asserts that it computes the SAME value
-- under a transformation of its inputs: exchange CC1 and CC2 -- which is
-- physically what happens when the user turns the plug over -- and every
-- output must be identical except `orientation`, which must invert.
--
-- That is checkable EXHAUSTIVELY over single transitions: 9 CC states x
-- 9 CC states = 81 ordered pairs, each replayed flipped and compared
-- sample by sample. A design that special-cased CC1 -- the natural way to
-- write it, and wrong -- fails the symmetry without failing any
-- single-orientation test.
--
-- 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, and takes its value from bits 30 downto 15 of
-- the state rather than the low bits: an LCG's low bits are phase-locked,
-- and reading them once in this track produced a perfectly uniform
-- histogram alongside zero of the events the phase existed to create.
--
-- THE MODEL IS A RUN-LENGTH THRESHOLD, NOT A STATE MACHINE. The design is
-- four states and a counter; the model counts how many consecutive ticks
-- the decoded situation has held and compares against DEBOUNCE. Same
-- behaviour, different formulation.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_tc_vhdl is
generic (
DIRECTED_ONLY : boolean := false
);
end entity;
architecture sim of tb_tc_vhdl is
constant DEBOUNCE : integer := 10;
constant L_OPEN : std_logic_vector(1 downto 0) := "00";
constant L_RA : std_logic_vector(1 downto 0) := "01";
constant L_RD : std_logic_vector(1 downto 0) := "10";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal tick : std_logic := '0';
signal cc1 : std_logic_vector(1 downto 0) := L_OPEN;
signal cc2 : std_logic_vector(1 downto 0) := L_OPEN;
signal attached : std_logic;
signal orientation : std_logic;
signal cable_powered : std_logic;
signal debug_accessory : std_logic;
signal n_attach : unsigned(15 downto 0);
signal n_detach : unsigned(15 downto 0);
signal n_glitch : unsigned(15 downto 0);
begin
dut : entity work.typec_cc_fsm
generic map (DEBOUNCE_TICKS => DEBOUNCE)
port map (
clk => clk, rst_n => rst_n, tick => tick,
cc1_st => cc1, cc2_st => cc2,
attached => attached, orientation => orientation,
cable_powered => cable_powered, debug_accessory => debug_accessory,
n_attach => n_attach, n_detach => n_detach, n_glitch => n_glitch
);
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;
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 @")); write(lo, now);
write(lo, string'(" step#")); write(lo, steps);
write(lo, string'(": ")); write(lo, what);
writeline(output, lo);
end if;
end if;
end procedure;
-- ---- the model, formulated as a run-length threshold ----
variable m_run : integer := 0;
variable m_attached : integer := 0;
variable m_orient : integer := 0;
variable m_cable : integer := 0;
variable m_debug : integer := 0;
variable m_att_c : integer := 0;
variable m_det_c : integer := 0;
variable m_gli_c : integer := 0;
function f_one_rd(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
if (a = L_RD) xor (b = L_RD) then return 1; else return 0; end if;
end function;
function f_both_rd(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
if a = L_RD and b = L_RD then return 1; else return 0; end if;
end function;
function f_cond(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
if f_one_rd(a,b) = 1 or f_both_rd(a,b) = 1 then return 1; else return 0; end if;
end function;
function f_orient(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
if b = L_RD then return 1; else return 0; end if;
end function;
function f_cable(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
if f_one_rd(a,b) = 0 then
return 0;
elsif a = L_RD then
if b = L_RA then return 1; else return 0; end if;
else
if a = L_RA then return 1; else return 0; end if;
end if;
end function;
-- The decoded SITUATION -- the three answers bundled, which is what the
-- debounce window actually applies to.
function f_sit(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) return integer is
begin
return f_both_rd(a,b) * 4 + f_cable(a,b) * 2 + f_orient(a,b);
end function;
procedure reset_dut is
begin
rst_n <= '0'; tick <= '0'; cc1 <= L_OPEN; cc2 <= L_OPEN;
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
m_run := 0; m_attached := 0; m_orient := 0; m_cable := 0; m_debug := 0;
m_att_c := 0; m_det_c := 0; m_gli_c := 0;
end procedure;
-- ---- change CC strictly BETWEEN ticks ----
--
-- CC is asynchronous to the debounce tick in real hardware, and letting
-- a change land on the same cycle as a tick makes "how many ticks has
-- this held for" ambiguous by one -- in the bench's bookkeeping, not in
-- the design. Keeping them apart removes the ambiguity rather than
-- papering over it.
procedure set_cc(a : std_logic_vector(1 downto 0);
b : std_logic_vector(1 downto 0)) is
variable was_cond, now_cond, was_sit, now_sit : integer;
begin
was_cond := f_cond(cc1, cc2);
now_cond := f_cond(a, b);
was_sit := f_sit(cc1, cc2);
now_sit := f_sit(a, b);
-- A glitch is an attach condition asserted and then WITHDRAWN before
-- it had held long enough to be believed. There is no "and it lasted
-- at least one tick" clause: the design enters its debounce state on
-- the clock edge after the condition appears, so a condition that
-- comes and goes between two ticks is still an abandoned attach.
if m_attached = 0 and was_cond = 1 and now_cond = 0 then
m_gli_c := m_gli_c + 1;
end if;
-- While UNATTACHED the window applies to the whole decoded situation:
-- a sink moving from one CC line to the other leaves attach_cond true
-- and still restarts the clock. While ATTACHED only the fact of
-- attachment matters.
if m_attached = 1 then
if was_cond /= now_cond then m_run := 0; end if;
else
if was_cond /= now_cond or was_sit /= now_sit then m_run := 0; end if;
end if;
cc1 <= a; cc2 <= b;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
end procedure;
-- ---- the trace, for the symmetry comparison ----
type trace_t is array (0 to 511) of std_logic_vector(3 downto 0);
variable tr_a : trace_t := (others => "0000");
variable tr_b : trace_t := (others => "0000");
variable tr_n : integer := 0;
variable recording : boolean := false;
procedure rec is
begin
if recording and tr_n < 512 then
tr_a(tr_n) := debug_accessory & cable_powered & orientation & attached;
tr_n := tr_n + 1;
end if;
end procedure;
procedure do_tick is
variable cond : integer;
begin
cond := f_cond(cc1, cc2);
tick <= '1';
wait until rising_edge(clk);
wait for 1 ns;
tick <= '0';
if cond /= m_attached then
m_run := m_run + 1;
if m_run = DEBOUNCE then
if cond = 1 then
m_attached := 1;
m_orient := f_orient(cc1, cc2);
m_cable := f_cable(cc1, cc2);
m_debug := f_both_rd(cc1, cc2);
m_att_c := m_att_c + 1;
else
m_attached := 0;
m_det_c := m_det_c + 1;
end if;
m_run := 0;
end if;
else
m_run := 0;
end if;
-- ---- PROPERTY 1: attach follows the model exactly ----
if m_attached = 1 then
ck(attached = '1', "attached disagrees with the run-length model");
else
ck(attached = '0', "attached disagrees with the run-length model");
end if;
-- ---- PROPERTY 2: the three latched answers are right ----
--
-- Checked only while attached: outside an attachment the spec gives
-- them no meaning, and asserting a value the design is free to hold
-- would be asserting an implementation detail rather than a contract.
if m_attached = 1 then
if m_orient = 1 then
ck(orientation = '1', "orientation disagrees with which line carried Rd");
else
ck(orientation = '0', "orientation disagrees with which line carried Rd");
end if;
if m_cable = 1 then
ck(cable_powered = '1', "cable_powered disagrees with Ra on the free line");
else
ck(cable_powered = '0', "cable_powered disagrees with Ra on the free line");
end if;
if m_debug = 1 then
ck(debug_accessory = '1', "debug_accessory disagrees with both lines at Rd");
else
ck(debug_accessory = '0', "debug_accessory disagrees with both lines at Rd");
end if;
end if;
-- ---- PROPERTY 3: the counters agree with the model's tally ----
ck(to_integer(n_attach) = m_att_c, "n_attach disagrees with the model");
ck(to_integer(n_detach) = m_det_c, "n_detach disagrees with the model");
ck(to_integer(n_glitch) = m_gli_c, "n_glitch disagrees with the model");
-- Sample the trace once per tick. Without this call tr_n stays at
-- zero, the comparison loop in phase 3 has an empty range, and the
-- symmetry property -- the whole point of the chapter -- is never
-- evaluated while every one of its checks reports success.
rec;
steps := steps + 1;
end procedure;
procedure ticks_n(n : integer) is
begin
for i in 1 to n loop
do_tick;
end loop;
end procedure;
-- ---- exhaustive reach over ordered pairs of CC states ----
type reach_t is array (0 to 80) of boolean;
variable reach : reach_t := (others => false);
variable nr : integer := 0;
type st_t is array (0 to 2) of std_logic_vector(1 downto 0);
constant ST : st_t := (L_OPEN, L_RA, L_RD);
-- The five CC combinations that constitute an attach, and they have
-- five DISTINCT decoded situations.
type att_t is array (0 to 4) of std_logic_vector(1 downto 0);
constant ATT1 : att_t := (L_OPEN, L_RA, L_RD, L_RD, L_RD);
constant ATT2 : att_t := (L_RD, L_RD, L_OPEN, L_RA, L_RD);
variable w, q : integer;
type accrow_t is array (0 to 4, 0 to 12) of integer;
variable acc : accrow_t := (others => (others => 0));
variable tab_n : integer := 0;
variable rnd_state : unsigned(31 downto 0) := x"00009117";
impure function urand return integer is
begin
-- resize is not optional: numeric_std's "*" on two 32-bit unsigneds
-- returns SIXTY-FOUR bits, and assigning that back is a fatal length
-- mismatch rather than the silent truncation Verilog would give.
rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
+ to_unsigned(12345, 32);
return to_integer(rnd_state(30 downto 15));
end function;
begin
reset_dut;
-- =============================================================
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- every ordered pair of CC
-- states, held long enough to be believed.
-- =============================================================
for i in 0 to 8 loop
for j in 0 to 8 loop
reset_dut;
set_cc(ST(i/3), ST(i mod 3));
ticks_n(DEBOUNCE + 2);
set_cc(ST(j/3), ST(j mod 3));
ticks_n(DEBOUNCE + 2);
reach(i*9 + j) := true;
end loop;
end loop;
-- =============================================================
-- PHASE 2 (DIRECTED, EXHAUSTIVE) -- the debounce threshold.
--
-- For each CC combination that constitutes an attach, present it for
-- every width from 0 to DEBOUNCE+2 ticks and then remove it. The
-- design must accept it if and only if the width reached DEBOUNCE.
-- Swept exhaustively because the interesting values are adjacent:
-- DEBOUNCE-1 must be rejected and DEBOUNCE accepted, and a suite that
-- tested 0 and 100 would pass with either bound wrong.
-- =============================================================
tab_n := 0;
for g in 0 to 8 loop
if f_cond(ST(g/3), ST(g mod 3)) = 1 then
for wi in 0 to DEBOUNCE + 2 loop
reset_dut;
set_cc(ST(g/3), ST(g mod 3));
ticks_n(wi);
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
-- ---- PROPERTY 4: the threshold is exactly DEBOUNCE ----
if wi >= DEBOUNCE then
ck(to_integer(n_attach) = 1,
"an attach was accepted at the wrong debounce width");
else
ck(to_integer(n_attach) = 0,
"an attach was accepted at the wrong debounce width");
end if;
-- ---- PROPERTY 5: a rejected attach is COUNTED as a glitch ----
--
-- Silently discarding a bouncing contact is the same behaviour
-- as never seeing it, and a bring-up engineer cannot tell a dead
-- cable from a bouncing one without this counter.
if wi > 0 and wi < DEBOUNCE then
ck(to_integer(n_glitch) = 1,
"a rejected attach was not counted as a glitch");
end if;
if tab_n < 5 then
if to_integer(n_attach) = 1 then
acc(tab_n, wi) := 1;
else
acc(tab_n, wi) := 0;
end if;
end if;
end loop;
tab_n := tab_n + 1;
end if;
end loop;
-- =============================================================
-- PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE SYMMETRY.
--
-- Replay all 81 ordered pairs with CC1 and CC2 exchanged, and require
-- the two traces to agree sample by sample on `attached`,
-- `cable_powered` and `debug_accessory`, and to DISAGREE on
-- `orientation` whenever a single-Rd sink is attached.
--
-- Why orientation is exempt when debug_accessory is set: with Rd on
-- BOTH lines there is no "other" line, so exchanging them changes
-- nothing and orientation is identical rather than inverted. That is
-- not a hole in the property -- it is the property, stated exactly.
-- =============================================================
for i in 0 to 8 loop
for j in 0 to 8 loop
reset_dut;
recording := true; tr_n := 0;
set_cc(ST(i/3), ST(i mod 3)); ticks_n(DEBOUNCE + 2);
set_cc(ST(j/3), ST(j mod 3)); ticks_n(DEBOUNCE + 2);
recording := false;
for p in 0 to tr_n - 1 loop
tr_b(p) := tr_a(p);
end loop;
q := tr_n;
reset_dut;
recording := true; tr_n := 0;
set_cc(ST(i mod 3), ST(i/3)); ticks_n(DEBOUNCE + 2);
set_cc(ST(j mod 3), ST(j/3)); ticks_n(DEBOUNCE + 2);
recording := false;
ck(tr_n = q, "the flipped run produced a different number of samples");
for p in 0 to q - 1 loop
if p < tr_n then
-- ---- PROPERTY 6: attach is orientation-INDEPENDENT ----
ck(tr_a(p)(0) = tr_b(p)(0),
"the plug turned over changed WHETHER something was attached");
-- ---- PROPERTY 7: so are the cable and debug answers ----
ck(tr_a(p)(2) = tr_b(p)(2),
"the plug turned over changed the powered-cable answer");
ck(tr_a(p)(3) = tr_b(p)(3),
"the plug turned over changed the debug-accessory answer");
-- ---- PROPERTY 8: orientation INVERTS, for a single-Rd sink --
if tr_b(p)(0) = '1' and tr_b(p)(3) = '0' then
ck(tr_a(p)(1) /= tr_b(p)(1),
"the plug turned over did NOT change the reported orientation");
end if;
-- ---- PROPERTY 9: and does NOT invert for a debug accessory --
if tr_b(p)(0) = '1' and tr_b(p)(3) = '1' then
ck(tr_a(p)(1) = tr_b(p)(1),
"a debug accessory reported a different orientation flipped");
end if;
end if;
end loop;
end loop;
end loop;
-- =============================================================
-- PHASE 4 (DIRECTED) -- a detach that changes its mind.
--
-- A momentary break while the cable is wiggled must NOT tear down the
-- attachment. `attached` has to stay high across the whole
-- detach-debounce window, and the attach counter must not advance
-- when the condition returns -- nothing was re-attached.
-- =============================================================
reset_dut;
set_cc(L_RD, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached = '1', "a fully debounced attach did not take");
for wi in 0 to DEBOUNCE - 1 loop
set_cc(L_OPEN, L_OPEN);
ticks_n(wi);
ck(attached = '1', "a momentary break tore down an established attachment");
set_cc(L_RD, L_OPEN);
ticks_n(2);
ck(attached = '1', "the attachment did not survive the break");
ck(to_integer(n_attach) = 1,
"a break that came back was counted as a second attach");
ck(to_integer(n_detach) = 0,
"a break that came back was counted as a detach");
end loop;
-- and now let it go for real
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached = '0', "a fully debounced detach did not take");
ck(to_integer(n_detach) = 1, "the real detach was not counted");
-- ---- PROPERTY 10: the SECOND attach gets the same window ----
--
-- Every other phase resets the DUT before each attach, so every one of
-- them is the first. That makes an entire class of bug invisible: a
-- debounce counter that is not cleared when the wait state is
-- re-entered still gives the first attach a full window and gives
-- every one after it whatever was left over.
--
-- Swept across the whole window rather than tested at one point: the
-- stale-counter bug leaves an ARBITRARY residue behind, so which width
-- exposes it depends on what the previous attachment happened to do.
for wi in 0 to DEBOUNCE loop
set_cc(L_RD, L_OPEN);
ticks_n(wi);
if wi >= DEBOUNCE then
ck(attached = '1',
"the second attach used a different debounce window from the first");
else
ck(attached = '0',
"the second attach used a different debounce window from the first");
end if;
set_cc(L_OPEN, L_OPEN);
ticks_n(DEBOUNCE + 2);
ck(attached = '0', "the port did not return to unattached");
end loop;
-- =============================================================
-- PHASE 5 (DIRECTED, EXHAUSTIVE) -- the situation changes DURING
-- the window, and the window restarts.
--
-- This is the phase that separates "debounce the fact of attachment"
-- from "debounce the CC state", and they are not the same thing. A
-- sink moving from CC1 to CC2 part-way through the window -- a plug
-- being rocked into place -- never drops `attach_cond`. A machine
-- watching only `attach_cond` would accept that attachment with an
-- orientation that had been stable for a single tick.
-- =============================================================
for p in 0 to 4 loop
for q2 in 0 to 4 loop
if p /= q2 then
for g in 0 to 2 loop
if g = 0 then w := 1;
elsif g = 1 then w := 3;
else w := DEBOUNCE - 1;
end if;
reset_dut;
set_cc(ATT1(p), ATT2(p));
ticks_n(w);
ck(attached = '0', "the first situation was accepted too early");
set_cc(ATT1(q2), ATT2(q2));
-- ---- PROPERTY 11: the window restarts from the CHANGE ----
ticks_n(DEBOUNCE - 1);
ck(attached = '0',
"a mid-window change of CC state did not restart the debounce");
do_tick;
ck(attached = '1',
"the restarted window did not complete one tick later");
-- ---- PROPERTY 12: the answers are the SECOND situation's ---
if f_orient(ATT1(q2), ATT2(q2)) = 1 then
ck(orientation = '1',
"the accepted orientation was the one that had already gone");
else
ck(orientation = '0',
"the accepted orientation was the one that had already gone");
end if;
if f_cable(ATT1(q2), ATT2(q2)) = 1 then
ck(cable_powered = '1', "the accepted cable answer was stale");
else
ck(cable_powered = '0', "the accepted cable answer was stale");
end if;
if f_both_rd(ATT1(q2), ATT2(q2)) = 1 then
ck(debug_accessory = '1', "the accepted debug answer was stale");
else
ck(debug_accessory = '0', "the accepted debug answer was stale");
end if;
-- ---- PROPERTY 13: no glitch was counted ----
--
-- Nothing was abandoned. The attach condition held throughout;
-- it was only ever a different attachment.
ck(to_integer(n_glitch) = 0,
"a change of situation was miscounted as an abandoned attach");
end loop;
end if;
end loop;
end loop;
-- =============================================================
-- PHASE 6 (RANDOM) -- a plug being pushed home.
--
-- Real insertion is not one clean edge. The contacts make and break
-- several times over a few milliseconds, in whatever order the
-- mechanics happen to produce, and both CC lines bounce
-- independently.
-- =============================================================
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 399 loop
set_cc(ST(urand mod 3), ST(urand mod 3));
-- 1..16 ticks, not 1..4. Against a ten-tick window a hold of at
-- most four can NEVER complete one, so the phase produced 400
-- events and zero attaches -- it exercised the glitch path and
-- nothing else.
ticks_n(1 + (urand mod 16));
end loop;
end if;
nr := 0;
for rj in 0 to 80 loop
if reach(rj) then nr := nr + 1; end if;
end loop;
write(lo, string'("steps=")); write(lo, steps);
write(lo, string'(" checks=")); write(lo, checks);
write(lo, string'(" reach=")); write(lo, nr); write(lo, string'("/81"));
write(lo, string'(" errors=")); write(lo, errors);
writeline(output, lo);
write(lo, string'("[typec] attaches=")); write(lo, to_integer(n_attach));
write(lo, string'(" detaches=")); write(lo, to_integer(n_detach));
write(lo, string'(" glitches=")); write(lo, to_integer(n_glitch));
writeline(output, lo);
write(lo, string'("--- was the attach accepted, by how long it was held ---"));
writeline(output, lo);
write(lo, string'(" ticks held "));
for wi in 0 to DEBOUNCE + 2 loop
write(lo, wi, right, 3);
end loop;
writeline(output, lo);
for p in 0 to 4 loop
write(lo, string'(" combo "));
write(lo, p);
write(lo, string'(" "));
for wi in 0 to DEBOUNCE + 2 loop
write(lo, acc(p, wi), right, 3);
end loop;
writeline(output, lo);
end loop;
if nr /= 81 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 ")); write(lo, checks);
write(lo, string'(" checks"));
else
write(lo, string'("FAIL: ")); write(lo, errors);
write(lo, string'(" errors in ")); write(lo, checks);
write(lo, string'(" checks"));
end if;
writeline(output, lo);
done <= true;
wait;
end process;
end architecture;9. Assertions
Four properties here are naturally temporal, written as SVA for a tool that supports it. Icarus does not — it rejects concurrent assertions outright — so each is enforced by the procedural check named beside it.
// ---- P1: attached never rises without a full window beneath it ----
//
// The property the whole module exists to provide. Stated as: attached
// cannot rise unless the tick before it was inside a debounce window that
// had already run its course.
property p_no_attach_without_window;
@(posedge clk) disable iff (!rst_n)
$rose(attached) |-> $past(state) == S_ATTACHWAIT &&
$past(ticks) == DEBOUNCE_TICKS - 1;
endproperty
// enforced procedurally by: PROPERTY 4, which sweeps every hold width from
// 0 to DEBOUNCE+2 for all five attach combinations and requires acceptance
// if and only if the width reached DEBOUNCE
// ---- P2: attached survives the whole detach window ----
//
// A momentary break must not tear down a power contract and a display
// link. This is the property a naive implementation gets wrong by
// deasserting on the first missing sample.
property p_attached_survives_a_break;
@(posedge clk) disable iff (!rst_n)
(attached && !attach_cond) |=> attached until_with
(state == S_UNATTACHED);
endproperty
// enforced procedurally by: PHASE 4, which breaks the contact for every
// width from 0 to DEBOUNCE-1 and requires attached to stay high throughout
// ---- P3: the latched answers never change while attached ----
//
// The three answers are decided once, at acceptance. A design that
// re-evaluated them continuously would track a CC glitch straight through
// to whatever is steering the high-speed mux.
property p_answers_are_stable;
@(posedge clk) disable iff (!rst_n)
attached |=> $stable(orientation) && $stable(cable_powered) &&
$stable(debug_accessory);
endproperty
// enforced procedurally by: PROPERTY 2, checked on every tick of every
// phase while attached, against a model that latches once
// ---- P4: the counters are monotonic ----
property p_counters_monotonic;
@(posedge clk) disable iff (!rst_n)
(n_attach >= $past(n_attach)) && (n_detach >= $past(n_detach)) &&
(n_glitch >= $past(n_glitch));
endproperty
// enforced procedurally by: PROPERTY 3, comparing all three against a
// model tally that is monotonic by construction, once per tick10. Where UVM Fits
For a port controller the trade genuinely flips, and for a specific reason: the attach machine is one agent in an environment that has to reach states no directed sequence would think to construct.
// =====================================================================
// The item is a CC EVENT -- a new pair of line levels, and how long to
// hold it. Not a transaction, because at this layer there are no
// transactions yet: that is the point of the layer.
// =====================================================================
class cc_event_item extends uvm_sequence_item;
`uvm_object_utils(cc_event_item)
rand cc_level_t cc1;
rand cc_level_t cc2;
rand int unsigned hold_ticks;
// ---- the constraint that makes this worth randomising ----
//
// Weighted toward the debounce boundary, because that is where every
// interesting behaviour is and uniform random would spend almost all of
// its time far away from it on both sides.
constraint c_hold {
hold_ticks dist {
0 :/ 5,
[1 : DEBOUNCE-2] :/ 20,
DEBOUNCE-1 :/ 20,
DEBOUNCE :/ 20,
[DEBOUNCE+1 : 3*DEBOUNCE] :/ 20
};
}
// 2'b11 is not a value of cc_level_t, so there is nothing to exclude.
endclass
// =====================================================================
// The scoreboard holds the run-length model -- deliberately NOT a state
// machine, so that a wrong state encoding in the DUT cannot be
// reproduced by the thing checking it.
// =====================================================================
class cc_scoreboard extends uvm_scoreboard;
`uvm_object_utils(cc_scoreboard)
uvm_analysis_imp #(cc_event_item, cc_scoreboard) ap;
int m_run;
bit m_attached;
bit m_orient, m_cable, m_debug;
function bit f_cond(cc_level_t a, cc_level_t b);
return ((a == CC_RD) ^ (b == CC_RD)) || ((a == CC_RD) && (b == CC_RD));
endfunction
function bit [2:0] f_sit(cc_level_t a, cc_level_t b);
bit one_rd = (a == CC_RD) ^ (b == CC_RD);
return { (a == CC_RD) && (b == CC_RD), // debug
one_rd ? ((a == CC_RD) ? (b == CC_RA) : (a == CC_RA)) : 1'b0,
(b == CC_RD) }; // orient
endfunction
...
endclass
// =====================================================================
// THE COVERAGE MODEL IS WHERE THIS DUT ACTUALLY NEEDS UVM.
// =====================================================================
covergroup cg_cc @(posedge tick);
cp_cc1 : coverpoint cc1 { bins open = {CC_OPEN}; bins ra = {CC_RA};
bins rd = {CC_RD}; }
cp_cc2 : coverpoint cc2 { bins open = {CC_OPEN}; bins ra = {CC_RA};
bins rd = {CC_RD}; }
// ---- the hold width, binned AROUND the boundary ----
//
// `just_short` and `exactly` are separate bins holding one value each.
// A range bin spanning both would report full coverage while never
// having distinguished them, which is the only distinction that matters.
cp_hold : coverpoint hold_ticks {
bins zero = {0};
bins short_hold = {[1 : DEBOUNCE-2]};
bins just_short = {DEBOUNCE-1};
bins exactly = {DEBOUNCE};
bins longer = {[DEBOUNCE+1 : $]};
}
// ---- the transition coverage that phase 5 exists for ----
//
// A change from one attach situation to a DIFFERENT one, mid-window.
// Neither endpoint is unusual; the transition between them is the event,
// and a coverpoint on states rather than transitions cannot see it.
cp_sit_change : coverpoint sit_now {
bins mid_window_change[] = ([0:5] => [0:5]);
}
x_orientation : cross cp_cc1, cp_cc2;
endcovergroup11. Mutation Testing
Nine mutations, each a plausible single mistake, each generated by a script that asserts its replacement applied.
MUT V-ALL V-DIR S-ALL S-DIR H-ALL H-DIR
BASE 0 0 0 0 0 0
S1 5896 2042 5896 2042 12066 2042
S2 8865 1352 8865 1352 10937 1352
S3 12317 1456 12317 1456 12257 1456
S4 1325 748 1325 748 1320 748
S5 11537 658 11537 658 12911 658
S6 8402 772 8402 772 10827 772
S7 959 597 959 597 944 597
S8 3855 832 3855 832 4285 832
S9 8712 1040 8712 1040 1734 1040 S1 orientation hard-wired to CC1 -- the natural way to write it, right
half the time, and the half it is wrong for is whichever way the
engineer did not hold the plug during bring-up
S2 the debounce window is one tick short
S3 a detach is not debounced at all
S4 `attached` drops while the detach is still debouncing
S5 the debounce counter is not cleared on entry to the wait state
S6 the window watches only WHETHER something is attached, not WHAT
S7 a debug accessory is reported as an ordinary sink
S8 an abandoned attach is not counted
S9 the powered-cable check reads the line the SINK is onEvery DIRECTED column is identical across all three languages and BASE reads
zero in all six. Every full column exceeds its directed column, and the two
Icarus columns differ from the VHDL one throughout — the random phase driving
different stimulus, which is expected and is the reason the decomposition exists.
The size of that gap is worth a glance. S9 is caught 8,712 times by the Verilog random phase and 1,734 times by the VHDL one: a factor of five, from the same design and the same property, purely because two generators happened to produce different numbers of powered-cable attachments. An ALL column is a fact about the stimulus at least as much as about the design, which is exactly why the directed columns are the ones required to match.
12. What This Does Not Cover
NOT MODELLED WHY IT IS OUT OF SCOPE
------------------------------- ------------------------------------
the VBUS switch and its a power path, not a decision
inrush limiting
the Rp level the port ADVERTISES the same two pins read the other way;
(500 mA / 1.5 A / 3.0 A) the sink's side of this chapter
Power Delivery: BMC encoding, an entire protocol stack that runs
message IDs, GoodCRC, hard reset AFTER this machine has finished
alternate-mode entry and the negotiated in PD structured VDMs
DisplayPort pin remap
MTP / PTP / ADB themselves class protocols over an ordinary bulk
pipe; 29.1 builds that shape
dual-role and role SWAP the machine here is source-side only
the analog comparators and RTL simulation has no voltages. The
their thresholds three levels are an INPUT here, and
whether the comparator that produces
them is right is not a question this
testbench can ask13. The Interview Answer
"USB-C is reversible. What does that actually cost the port controller, and how would you verify it?"
It costs one bit of state and a symmetry obligation on everything downstream.
The connector is reversible because the sink's Rd resistor lands on CC1 or CC2 depending on insertion, and the port learns the orientation by seeing which of its two CC lines is pulled down. Exactly one, not at least one — both pulled down is a debug accessory, and driving VBUS into one is a real interop failure. That single bit then steers the high-speed mux, so everything downstream inherits it.
The verification answer is the interesting half. The obvious approach is to test both orientations and check the answers, and that is weaker than it looks, because the natural buggy implementation — look at CC1, fall back to CC2 — gives a correct answer in one orientation. What you want instead is the symmetry: run any input sequence, run it again with the two CC lines exchanged, and require every output to be identical except the orientation bit, which must invert.
That is exhaustively checkable, because the transformation is finite and total: there are two orientations and every input has a mirror. Here it is 81 ordered pairs of CC states, each replayed flipped and compared sample by sample. It costs one extra run of a sequence you are already running, and it catches a class of bug no single-orientation test can.
Two things worth adding unprompted. First, the debounce is on the CC state, not on the fact of attachment — a sink moving from CC1 to CC2 mid-window never drops "something is attached", so a machine watching only that bit will accept an orientation that was stable for one tick. Second, the symmetry is not expressible in SVA: it relates two executions, and an assertion language can only talk about one. It has to be a testbench structure.
14. What Carries Forward
THE MECHANISM
o two pins, three levels, nine combinations, five of which are an attach
o EXACTLY one Rd is a sink; both is a debug accessory, and the difference
is one character of RTL
o the orientation is not signalled anywhere -- it IS which line had the Rd
o the debounce applies to the CC STATE, not to the fact of attachment
o a detach is debounced too, and `attached` stays up throughout
o a correct debounce makes the sampling instant irrelevant, which turns a
real question into a non-question
THE RESULT
o the accept threshold is a step function at exactly the window width,
identical for all five attach combinations
o the symmetry holds over all 81 ordered CC transitions in three
languages: everything identical, orientation inverted, and identical
again for a debug accessory because there is no other line to swap
THE METHOD
o a SYMMETRY is exhaustively checkable when the transformation is finite
and total, and is strictly stronger than any number of value checks
o a symmetry is NOT expressible in SVA -- it relates two executions
o IDENTICAL SCORES from two different mutations mean the mutants are
behaviourally the same, or one check is doing all the work for both;
either way it is a signal
o a loop whose bound is never incremented has an empty range, and every
check inside it passes by not existing -- 6,507 of them here
o if every phase resets before the scenario, every scenario is the FIRST
one, and an entire class of bug is unreachable
o sweep a boundary rather than probing it: a stale counter leaves an
arbitrary residue, so one probe finds some and misses others
o VHDL declaration order asks "who calls this", and Verilog does not
o RTL cannot evaluate an analog decode -- say what the result is
conditional onThe next chapter drops to the other end of the scale: no operating system, no class driver, a few kilobytes of RAM, and a USB stack that has to fit in it.
Continue learning
Related tutorials
- 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.
- Related topic
USB vs SPI
SPI selects a peripheral with a wire routed at layout time and USB with an address the host assigned — so a chip-select contention is invisible to every slave (0 of 11) while a duplicate USB address is detected every time (274 of 274).
- Related topic
USB vs Ethernet
USB has one authority that assigns every address; Ethernet has none, so a switch infers the topology from traffic — and an inferred table is wrong 294 times out of 1065 where an assigned one is wrong 0 times out of 130.
- Related topic
USB vs PCIe
USB holds one transaction outstanding per endpoint so its throughput is exactly 1/(latency+1) whatever the wire carries; PCIe tags many at once and needs exactly latency+1 tags to saturate — both measured as closed forms over 64 points.
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.
