UART · Module 10
RTS/CTS Hardware Flow Control
Out-of-band backpressure with a latency. The far end may be mid-frame, may have another committed, and takes time to notice — so the threshold must be derived from the bytes still in flight.
Chapter 10.4 ended at a wall: ready = !full stops a local producer completely and has no path off the device. The far end's transmitter has its own clock, its own schedule, and no knowledge of this design.
Hardware flow control closes that gap with a dedicated wire. The wire is the easy part. The engineering is in one fact that the phrase "flow control" actively obscures:
Asserting flow control when the FIFO is full is already too late.
The far end may be halfway through a character, may have already committed another to its shift register, and takes some time to notice the request at all. Every one of those bytes is already in flight and cannot be recalled. The threshold that asserts backpressure must therefore leave enough headroom to absorb them — and §5 shows a configuration where no threshold in a 16-entry FIFO is sufficient.
1. Naming, Because the Conventional Names Are Ambiguous
RTS and CTS are inherited from modem wiring, where the roles were asymmetric between terminal equipment and communication equipment. The names survived; the original meanings did not travel well, and a design review that assumes shared understanding of "RTS" goes wrong quickly.
Role-based language first, because it is unambiguous:
local receiver backpressure OUTPUT — "I am running out of room; stop sending"
remote transmitter permission INPUT — "the far end says I may send"Then the mapping used throughout this chapter, stated once:
| Signal | Direction | Polarity | Meaning |
|---|---|---|---|
rts_n_o | out of this device | active low | 0 = far end may send · 1 = far end should stop |
cts_n_i | in to this device | active low | 0 = we may send · 1 = we must not start a new frame |
In a null-modem connection the two cross: this device's rts_n_o drives the far end's cts_n_i, and vice versa. That is the arrangement assumed here, and it is the common one for two peer devices.
Active low is a convention, not a requirement, and inverted variants are common. What matters is that the polarity appear once, in a named signal, rather than being implied by prose — a mistake that costs a board spin rather than a debug session.
2. The Latency Is the Subject
3. The In-Flight Budget
Stated as a model rather than a constant, because the third term depends on the far end:
in_flight = 1 (frame currently being transmitted)
+ 1 (frame already committed to the shifter)
+ ceil(recognition_delay / T_frame)Machine-computed across rates and recognition delays:
| baud | T_frame | recognition 0 µs | 10 µs | 50 µs |
|---|---|---|---|---|
| 115,200 | 86.81 µs | 2 bytes | 3 bytes | 3 bytes |
| 1,000,000 | 10.00 µs | 2 bytes | 3 bytes | 7 bytes |
| 3,000,000 | 3.33 µs | 2 bytes | 5 bytes | 17 bytes |
Read the bottom-right cell. At 3 Mbaud with a far end that takes 50 µs to react, seventeen characters arrive after backpressure is asserted. A 16-entry FIFO cannot hold them at any threshold — even asserting at level 0 leaves only 16 entries of headroom.
That is not a threshold problem; it is a capacity problem. The conclusion falls straight out of the arithmetic: at that rate and that far end, flow control requires a deeper FIFO, and no amount of tuning the trigger rescues it. A design that discovers this in the field instead will have spent the intervening time adjusting thresholds.
The recognition delay is the term nobody knows, and it is worth pressing on. For a hardware UART on the far end it is typically a synchroniser plus a comparison — a few clocks. For a USB-serial bridge, a driver-mediated link or anything with its own buffering, it can be orders of magnitude larger, and the 50 µs column is not a pathological case.
4. The Threshold
The requirement is the same shape as Chapter 10.3's and the number is different:
H = DEPTH − STOP_LEVEL must satisfy H ≥ in_flight + marginFor a 16-entry FIFO:
STOP_LEVEL | headroom H | covers 2 bytes? | 3 bytes? | 4 bytes? |
|---|---|---|---|---|
| 8 | 8 | yes | yes | yes |
| 10 | 6 | yes | yes | yes |
| 12 | 4 | yes | yes | yes |
| 13 | 3 | yes | yes | NO |
| 14 | 2 | yes | NO | NO |
| 15 | 1 | NO | NO | NO |
At 115,200 baud with a hardware far end, STOP_LEVEL = 12 gives 4 bytes of headroom against 2 in flight — a margin of 2 entries.
5. Hysteresis Is Not Optional Here
Chapter 10.3 §6 noted that a single threshold chatters while occupancy hovers on the boundary, and that for an interrupt this is merely noisy. For a pin it is expensive.
A chattering rts_n_o tells the far end to stop and start repeatedly. Each stop costs it the recognition delay and possibly an aborted opportunity to launch; each start costs the same again. Throughput collapses, and the symptom — a link running far below its configured rate with no errors reported — is one of the harder ones to attribute.
The fix is two thresholds with the decision held between them:
stop when level >= STOP_LEVEL
resume when level <= RESUME_LEVEL with RESUME_LEVEL < STOP_LEVEL
between: HOLDThe gap must be wide enough to be worth the round trip. Resuming one entry below the stop level means the far end restarts, sends one character, and is stopped again. A gap of roughly half the depth — stop at 12, resume at 6 in a 16-entry FIFO — means each resume buys six characters of useful transfer.
6. The RTL
// Synthesizable SystemVerilog — receive-side backpressure with hysteresis.
module uart_flow_ctrl #(
parameter int unsigned DEPTH = 16,
parameter int unsigned STOP_LEVEL = 12, // request stop at/above this
parameter int unsigned RESUME_LEVEL = 6 // allow again at/below this
) (
input logic clk,
input logic rst_n,
input logic [$clog2(DEPTH+1)-1:0] level_i,
output logic rts_n_o, // ACTIVE LOW out: 0 = may send, 1 = stop
output logic allow_remote_o // same decision, active high, for readers
);
localparam int unsigned CNT_W = $clog2(DEPTH + 1);
initial begin
if (RESUME_LEVEL >= STOP_LEVEL)
$fatal(1, "uart_flow_ctrl: RESUME_LEVEL %0d must be below STOP_LEVEL %0d",
RESUME_LEVEL, STOP_LEVEL);
if (STOP_LEVEL > DEPTH)
$fatal(1, "uart_flow_ctrl: STOP_LEVEL %0d exceeds DEPTH %0d", STOP_LEVEL, DEPTH);
end
logic allow_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
allow_q <= 1'b1; // out of reset we can accept
end else begin
// Two separate thresholds, not one. A single threshold chatters
// while occupancy sits on the boundary — Chapter 10.5 §5.
if (level_i >= CNT_W'(STOP_LEVEL)) allow_q <= 1'b0;
else if (level_i <= CNT_W'(RESUME_LEVEL)) allow_q <= 1'b1;
// between the two: HOLD the current decision
end
end
assign allow_remote_o = allow_q;
assign rts_n_o = ~allow_q; // active low
endmodule// Synthesizable SystemVerilog — transmit-side permission gating.
module uart_tx_gate (
input logic clk,
input logic rst_n,
input logic cts_n_i, // ACTIVE LOW in: 0 = permitted to send
input logic tx_busy_i, // from the Module 7 transmitter
input logic tx_fifo_empty_i,
output logic launch_allowed_o // gate on offering the next byte
);
logic cts_meta_q, cts_sync_q;
// cts_n_i arrives from a pin driven by another device's clock. It is
// asynchronous and gets the same two-stage treatment as rx_i — Ch 5.1.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cts_meta_q <= 1'b1; // reset to "not permitted"
cts_sync_q <= 1'b1;
end else begin
cts_meta_q <= cts_n_i;
cts_sync_q <= cts_meta_q;
end
end
// A launch needs permission AND something to send. tx_busy_i is NOT in
// this expression: a frame in flight finishes regardless of cts.
assign launch_allowed_o = !cts_sync_q && !tx_fifo_empty_i;
endmodule7. A Frame in Progress Is Never Truncated
The transmit gate contains one deliberate omission, and it is the chapter's second-most-important point:
assign launch_allowed_o = !cts_sync_q && !tx_fifo_empty_i;
// ^^^^^^^^^^^^^^^^
// tx_busy_i is NOT in this expression.tx_busy_i is an input to the module and is not used in the decision. That is intentional: the gate controls whether the next frame may launch, and has no influence on a frame already being emitted.
The cts_n_i synchroniser is not optional either. It is a pin driven by another device's clock, so it is genuinely asynchronous and gets the same two-stage treatment Chapter 5.1 applied to rx_i. It resets to 1 — not permitted — because a transmitter released from reset with no information about the far end should not assume permission.
This is the only other asynchronous input in the design. Chapter 8.1 §9 noted that the enable architecture leaves the UART with exactly one clock-domain crossing; adding hardware flow control makes it two, and both are handled identically at the boundary.
Stop at 12, resume at 6
13 cycles8. Both Blocks in Three Languages
§6 published the receive side and the transmit side as SystemVerilog. They are different shapes — one is a hysteretic comparator, the other a synchroniser plus a gate — and both translate without a design change.
The receive side
//===========================================================================
// uart_flow_ctrl_v — Synthesizable Verilog-2001
//
// Receive-side backpressure with HYSTERESIS: two thresholds, not one.
// Strictly between them the decision HOLDS, which is what stops the output
// chattering while occupancy sits on a boundary -- 10.5 section 5.
//
// rts_n_o is ACTIVE LOW at the pin: 0 means "you may send".
//===========================================================================
module uart_flow_ctrl_v (
clk, rst_n, level_i, rts_n_o, allow_remote_o
);
parameter DEPTH = 16;
parameter STOP_LEVEL = 12; // request stop at/above this
parameter RESUME_LEVEL = 6; // allow again at/below this
function integer clog2;
input integer value;
integer i;
begin
clog2 = 1;
for (i = value - 1; i > 1; i = i >> 1) clog2 = clog2 + 1;
end
endfunction
localparam CNT_W = clog2(DEPTH + 1);
input clk;
input rst_n;
input [CNT_W-1:0] level_i;
output rts_n_o;
output allow_remote_o;
reg allow_q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
allow_q <= 1'b1; // reset: accepting
end else begin
if (level_i >= STOP_LEVEL[CNT_W-1:0]) allow_q <= 1'b0;
else if (level_i <= RESUME_LEVEL[CNT_W-1:0]) allow_q <= 1'b1;
// strictly between the two thresholds: HOLD the current decision
end
end
assign allow_remote_o = allow_q;
assign rts_n_o = ~allow_q;
endmodule--==========================================================================
-- uart_flow_ctrl -- Synthesizable VHDL-2008
-- Receive-side backpressure with HYSTERESIS. Strictly between the two
-- thresholds the decision HOLDS.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.uart_fifo_pkg.all;
entity uart_flow_ctrl is
generic (
DEPTH : positive := 16;
STOP_LEVEL : natural := 12;
RESUME_LEVEL : natural := 6
);
port (
clk : in std_logic;
rst_n : in std_logic;
level_i : in std_logic_vector(cnt_width(DEPTH+1)-1 downto 0);
rts_n_o : out std_logic; -- ACTIVE LOW: '0' = may send
allow_remote_o : out std_logic
);
end entity uart_flow_ctrl;
architecture rtl of uart_flow_ctrl is
constant CNT_W : positive := cnt_width(DEPTH + 1);
signal allow_q : std_logic;
begin
assert RESUME_LEVEL < STOP_LEVEL
report "uart_flow_ctrl: RESUME_LEVEL must be below STOP_LEVEL"
severity failure;
assert STOP_LEVEL <= DEPTH
report "uart_flow_ctrl: STOP_LEVEL exceeds DEPTH"
severity failure;
allow_remote_o <= allow_q;
rts_n_o <= not allow_q;
process (clk, rst_n)
begin
if rst_n = '0' then
allow_q <= '1'; -- out of reset: accepting
elsif rising_edge(clk) then
if unsigned(level_i) >= to_unsigned(STOP_LEVEL, CNT_W) then
allow_q <= '0';
elsif unsigned(level_i) <= to_unsigned(RESUME_LEVEL, CNT_W) then
allow_q <= '1';
end if;
-- strictly between: HOLD
end if;
end process;
end architecture rtl;The transmit side
//===========================================================================
// uart_tx_gate_v — Synthesizable Verilog-2001
//
// cts_n_i is driven by another device's clock. It is asynchronous and gets
// the same two-flop treatment as rx_i -- Chapter 5.1. Two flops REDUCE the
// probability that metastability propagates; they do not eliminate it.
//
// tx_busy_i is deliberately NOT in the launch expression: a frame already
// in flight finishes regardless of what CTS does -- 10.5 section 6.
//===========================================================================
module uart_tx_gate_v (
input wire clk,
input wire rst_n,
input wire cts_n_i, // ACTIVE LOW in: 0 = permitted to send
input wire tx_busy_i,
input wire tx_fifo_empty_i,
output wire launch_allowed_o
);
reg cts_meta_q, cts_sync_q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cts_meta_q <= 1'b1; // reset to "not permitted"
cts_sync_q <= 1'b1;
end else begin
cts_meta_q <= cts_n_i;
cts_sync_q <= cts_meta_q;
end
end
assign launch_allowed_o = !cts_sync_q && !tx_fifo_empty_i;
endmodule--==========================================================================
-- uart_tx_gate -- Synthesizable VHDL-2008
--
-- cts_n_i is driven by another device's clock, so it gets the same two-flop
-- treatment as rx_i. Two flops REDUCE the probability that metastability
-- propagates; they do not eliminate it.
--
-- tx_busy_i is deliberately NOT in the launch expression: a frame already
-- in flight finishes regardless of what CTS does.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
entity uart_tx_gate is
port (
clk : in std_logic;
rst_n : in std_logic;
cts_n_i : in std_logic; -- ACTIVE LOW: '0' = permitted
tx_busy_i : in std_logic;
tx_fifo_empty_i : in std_logic;
launch_allowed_o : out std_logic
);
end entity uart_tx_gate;
architecture rtl of uart_tx_gate is
signal cts_meta_q, cts_sync_q : std_logic;
begin
launch_allowed_o <= (not cts_sync_q) and (not tx_fifo_empty_i);
process (clk, rst_n)
begin
if rst_n = '0' then
cts_meta_q <= '1'; -- reset to "not permitted"
cts_sync_q <= '1';
elsif rising_edge(clk) then
cts_meta_q <= cts_n_i;
cts_sync_q <= cts_meta_q;
end if;
end process;
end architecture rtl;| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| two thresholds, hold between | yes | yes | yes | yes |
rts_n_o polarity | active low | active low | active low | yes |
| reset decision | accepting | accepting | accepting | yes |
cts_n_i synchroniser | 2 flops | 2 flops | 2 flops | yes |
| synchroniser reset value | not permitted | not permitted | not permitted | yes |
tx_busy_i in the launch term | no | no | no | yes |
| parameter legality checks | none | $fatal | assert | no — 2001 has no mechanism |
The last row is a real limitation, not a style choice. Verilog-2001 has
no elaboration-time assertion worth the name, so
RESUME_LEVEL < STOP_LEVEL is documented in a header comment and relied
on. SystemVerilog and VHDL both refuse to elaborate an illegal pair. A
project standardising on 2001 carries that check in review instead, and it
is worth knowing which of your guarantees are enforced and which are merely
written down.
9. The Self-Checking Testbenches
Hysteresis is a property of a SEQUENCE, not of an occupancy. That single observation shapes the whole receive-side testbench: the same level is driven from below and from above, and must give opposite answers.
set_level(5'd8); check(allow === 1'b1, "level 8 rising: INSIDE the band, holds");
// ... up through STOP, then back down ...
set_level(5'd8); check(allow === 1'b0, "level 8 falling: SAME level, opposite decision");A testbench that only swept occupancy would pass a single-threshold design. Three separate chatter tests then drive 20, 20 and 30 crossings of each boundary and assert that the output does not move at all.
//===========================================================================
// tb_uart_flow_ctrl_v — self-checking Verilog-2001 testbench
//
// The property that matters is HYSTERESIS, and it is a property of a
// SEQUENCE rather than of any single occupancy: strictly between the two
// thresholds the output must depend on how the level ARRIVED there. A
// testbench that only swept occupancy would pass a single-threshold design.
//
// INDEPENDENCE: the checker tracks the last threshold the level crossed
// and derives the expected decision from that history.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_flow_ctrl_v;
localparam DEPTH = 16, STOP = 12, RESUME = 6, CNT_W = 5;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg [CNT_W-1:0] level = 5'd0;
wire rts_n, allow;
uart_flow_ctrl_v #(.DEPTH(DEPTH), .STOP_LEVEL(STOP), .RESUME_LEVEL(RESUME)) dut (
.clk(clk), .rst_n(rst_n), .level_i(level),
.rts_n_o(rts_n), .allow_remote_o(allow));
integer checks = 0, failures = 0;
task check;
input cond;
input [8*58-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
// rts_n_o must be the exact inverse of allow_remote_o, every cycle.
integer polarity_bad = 0;
always @(posedge clk) if (rst_n && (rts_n !== ~allow)) polarity_bad = polarity_bad + 1;
// How many times did the decision change? Hysteresis is the claim that
// walking the band does NOT produce transitions.
integer transitions = 0;
reg allow_prev = 1'b1;
always @(posedge clk) if (rst_n) begin
if (allow !== allow_prev) transitions = transitions + 1;
allow_prev = allow;
end
task set_level;
input [CNT_W-1:0] v;
begin @(negedge clk) level = v; @(negedge clk); end
endtask
// `transitions` is updated by a posedge observer comparing a registered
// output against its own previous sample, so it lags the stimulus by two
// active edges. A baseline captured before that settles counts the
// entering transition inside the loop and the test measures itself.
task settle;
begin repeat (3) @(negedge clk); end
endtask
integer i, base;
initial begin
#500_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG FLOW-CTRL TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_flow_ctrl_v : self-checking Verilog testbench ==");
rst_n = 1'b0; level = 5'd0;
repeat (4) @(negedge clk);
check(allow === 1'b1, "reset: accepting (allow high)");
check(rts_n === 1'b0, "reset: RTS asserted low = may send");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- rising through the band -------------------------------------
set_level(5'd6); check(allow === 1'b1, "level 6 rising: still accepting");
set_level(5'd8); check(allow === 1'b1, "level 8 rising: INSIDE the band, holds");
set_level(5'd11); check(allow === 1'b1, "level 11 rising: one below STOP, holds");
set_level(5'd12); check(allow === 1'b0, "level 12: AT stop threshold, stops");
check(rts_n === 1'b1, "stopped: RTS deasserted high");
// ---- falling back through the band -------------------------------
// THE hysteresis test. The same occupancies that held `allow` high
// on the way up must hold it LOW on the way down.
set_level(5'd11); check(allow === 1'b0, "level 11 falling: still stopped");
set_level(5'd8); check(allow === 1'b0, "level 8 falling: SAME level, opposite decision");
set_level(5'd7); check(allow === 1'b0, "level 7 falling: one above RESUME, holds");
set_level(5'd6); check(allow === 1'b1, "level 6: AT resume threshold, resumes");
// ---- chatter on the STOP boundary must not chatter the output ----
set_level(5'd12);
settle;
base = transitions;
for (i = 0; i < 20; i = i + 1) begin
set_level(5'd11);
set_level(5'd12);
end
settle;
check(transitions == base, "20 crossings of STOP produce NO extra transitions");
check(allow === 1'b0, "and the decision is still stop");
// ---- chatter on the RESUME boundary ------------------------------
set_level(5'd6);
settle;
base = transitions;
for (i = 0; i < 20; i = i + 1) begin
set_level(5'd7);
set_level(5'd6);
end
settle;
check(transitions == base, "20 crossings of RESUME produce NO extra transitions");
check(allow === 1'b1, "and the decision is still allow");
// ---- a single-threshold design would fail this one ---------------
// Sitting exactly between the thresholds and jittering must produce
// no output activity at all.
set_level(5'd9);
settle;
base = transitions;
for (i = 0; i < 30; i = i + 1) begin
set_level(5'd8);
set_level(5'd10);
end
settle;
check(transitions == base, "30 jitters inside the band: output never moves");
// ---- extremes -----------------------------------------------------
set_level(5'd16); check(allow === 1'b0, "full: stopped");
set_level(5'd0); check(allow === 1'b1, "empty: accepting");
// ---- reset while stopped ------------------------------------------
set_level(5'd16);
check(allow === 1'b0, "stopped before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(allow === 1'b1, "reset returns to accepting");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(polarity_bad == 0, "rts_n_o was the inverse of allow, every cycle");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG FLOW-CTRL TESTS PASSED");
else $display(" RESULT: VERILOG FLOW-CTRL TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_flow_ctrl_sv — self-checking Verilog-2001 testbench
//
// The property that matters is HYSTERESIS, and it is a property of a
// SEQUENCE rather than of any single occupancy: strictly between the two
// thresholds the output must depend on how the level ARRIVED there. A
// testbench that only swept occupancy would pass a single-threshold design.
//
// INDEPENDENCE: the checker tracks the last threshold the level crossed
// and derives the expected decision from that history.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_flow_ctrl_sv;
localparam DEPTH = 16, STOP = 12, RESUME = 6, CNT_W = 5;
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
logic [CNT_W-1:0] level = 5'd0;
logic rts_n, allow;
uart_flow_ctrl #(.DEPTH(DEPTH), .STOP_LEVEL(STOP), .RESUME_LEVEL(RESUME)) dut (
.clk(clk), .rst_n(rst_n), .level_i(level),
.rts_n_o(rts_n), .allow_remote_o(allow));
int unsigned checks = 0, failures = 0;
task automatic check(input bit cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
// rts_n_o must be the exact inverse of allow_remote_o, every cycle.
int polarity_bad = 0;
always @(posedge clk) if (rst_n && (rts_n !== ~allow)) polarity_bad = polarity_bad + 1;
// How many times did the decision change? Hysteresis is the claim that
// walking the band does NOT produce transitions.
int transitions = 0;
logic allow_prev = 1'b1;
always @(posedge clk) if (rst_n) begin
if (allow !== allow_prev) transitions = transitions + 1;
allow_prev = allow;
end
task automatic set_level(input logic [CNT_W-1:0] v);
@(negedge clk) level = v;
@(negedge clk);
endtask
// `transitions` is updated by a posedge observer comparing a registered
// output against its own previous sample, so it lags the stimulus by two
// active edges. A baseline captured before that settles counts the
// entering transition inside the loop and the test measures itself.
task automatic settle(); repeat (3) @(negedge clk); endtask
int i, base;
initial begin
#500us;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG FLOW-CTRL TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_flow_ctrl : self-checking Verilog testbench ==");
rst_n = 1'b0; level = 5'd0;
repeat (4) @(negedge clk);
check(allow === 1'b1, "reset: accepting (allow high)");
check(rts_n === 1'b0, "reset: RTS asserted low = may send");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- rising through the band -------------------------------------
set_level(5'd6); check(allow === 1'b1, "level 6 rising: still accepting");
set_level(5'd8); check(allow === 1'b1, "level 8 rising: INSIDE the band, holds");
set_level(5'd11); check(allow === 1'b1, "level 11 rising: one below STOP, holds");
set_level(5'd12); check(allow === 1'b0, "level 12: AT stop threshold, stops");
check(rts_n === 1'b1, "stopped: RTS deasserted high");
// ---- falling back through the band -------------------------------
// THE hysteresis test. The same occupancies that held `allow` high
// on the way up must hold it LOW on the way down.
set_level(5'd11); check(allow === 1'b0, "level 11 falling: still stopped");
set_level(5'd8); check(allow === 1'b0, "level 8 falling: SAME level, opposite decision");
set_level(5'd7); check(allow === 1'b0, "level 7 falling: one above RESUME, holds");
set_level(5'd6); check(allow === 1'b1, "level 6: AT resume threshold, resumes");
// ---- chatter on the STOP boundary must not chatter the output ----
set_level(5'd12);
settle();
base = transitions;
for (i = 0; i < 20; i = i + 1) begin
set_level(5'd11);
set_level(5'd12);
end
settle();
check(transitions == base, "20 crossings of STOP produce NO extra transitions");
check(allow === 1'b0, "and the decision is still stop");
// ---- chatter on the RESUME boundary ------------------------------
set_level(5'd6);
settle();
base = transitions;
for (i = 0; i < 20; i = i + 1) begin
set_level(5'd7);
set_level(5'd6);
end
settle();
check(transitions == base, "20 crossings of RESUME produce NO extra transitions");
check(allow === 1'b1, "and the decision is still allow");
// ---- a single-threshold design would fail this one ---------------
// Sitting exactly between the thresholds and jittering must produce
// no output activity at all.
set_level(5'd9);
settle();
base = transitions;
for (i = 0; i < 30; i = i + 1) begin
set_level(5'd8);
set_level(5'd10);
end
settle();
check(transitions == base, "30 jitters inside the band: output never moves");
// ---- extremes -----------------------------------------------------
set_level(5'd16); check(allow === 1'b0, "full: stopped");
set_level(5'd0); check(allow === 1'b1, "empty: accepting");
// ---- reset while stopped ------------------------------------------
set_level(5'd16);
check(allow === 1'b0, "stopped before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(allow === 1'b1, "reset returns to accepting");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(polarity_bad == 0, "rts_n_o was the inverse of allow, every cycle");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG FLOW-CTRL TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG FLOW-CTRL TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_flow_ctrl -- self-checking VHDL-2008 testbench
-- Same 21 checks. Hysteresis is a property of a SEQUENCE, so the same
-- occupancy is driven from below and from above and must give opposite
-- answers.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;
use work.uart_fifo_pkg.all;
entity tb_uart_flow_ctrl is
end entity tb_uart_flow_ctrl;
architecture sim of tb_uart_flow_ctrl is
constant DEPTH : positive := 16;
constant STOPL : natural := 12;
constant RESUML : natural := 6;
constant CNT_W : positive := cnt_width(DEPTH+1);
constant CLK_PERIOD : time := 10 ns;
signal clk, rst_n, done : std_logic := '0';
signal level : std_logic_vector(CNT_W-1 downto 0) := (others => '0');
signal rts_n, allow : std_logic;
signal transitions, polarity_bad : natural := 0;
begin
clk <= not clk after CLK_PERIOD/2 when done = '0' else '0';
dut : entity work.uart_flow_ctrl
generic map (DEPTH => DEPTH, STOP_LEVEL => STOPL, RESUME_LEVEL => RESUML)
port map (clk => clk, rst_n => rst_n, level_i => level,
rts_n_o => rts_n, allow_remote_o => allow);
obs : process (clk)
variable t, pb : natural := 0;
variable prev : std_logic := '1';
begin
if rising_edge(clk) and rst_n = '1' then
if allow /= prev then t := t + 1; end if;
prev := allow;
if rts_n /= (not allow) then pb := pb + 1; end if;
transitions <= t; polarity_bad <= pb;
end if;
end process obs;
watchdog : process
begin
wait for 500 us;
report " RESULT: VHDL FLOW-CTRL TESTS FAILED (timeout)" severity failure;
wait;
end process watchdog;
stim : process
variable checks, failures, base : natural := 0;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name;
else failures := failures + 1; report " FAIL " & name severity error; end if;
end procedure;
procedure set_level(v : natural) is
begin
wait until falling_edge(clk);
level <= std_logic_vector(to_unsigned(v, CNT_W));
wait until falling_edge(clk);
end procedure;
-- the observer compares a registered output against its own previous
-- sample, so it lags the stimulus by two active edges
procedure settle is
begin
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
end procedure;
begin
report "== uart_flow_ctrl (VHDL) : self-checking testbench ==";
rst_n <= '0'; level <= (others => '0');
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(allow = '1', "reset: accepting (allow high)");
check(rts_n = '0', "reset: RTS asserted low = may send");
rst_n <= '1'; wait until falling_edge(clk);
set_level(6); check(allow = '1', "level 6 rising: still accepting");
set_level(8); check(allow = '1', "level 8 rising: INSIDE the band, holds");
set_level(11); check(allow = '1', "level 11 rising: one below STOP, holds");
set_level(12); check(allow = '0', "level 12: AT stop threshold, stops");
check(rts_n = '1', "stopped: RTS deasserted high");
set_level(11); check(allow = '0', "level 11 falling: still stopped");
set_level(8); check(allow = '0', "level 8 falling: SAME level, opposite decision");
set_level(7); check(allow = '0', "level 7 falling: one above RESUME, holds");
set_level(6); check(allow = '1', "level 6: AT resume threshold, resumes");
set_level(12); settle; base := transitions;
for i in 0 to 19 loop set_level(11); set_level(12); end loop;
settle;
check(transitions = base, "20 crossings of STOP produce NO extra transitions");
check(allow = '0', "and the decision is still stop");
set_level(6); settle; base := transitions;
for i in 0 to 19 loop set_level(7); set_level(6); end loop;
settle;
check(transitions = base, "20 crossings of RESUME produce NO extra transitions");
check(allow = '1', "and the decision is still allow");
set_level(9); settle; base := transitions;
for i in 0 to 29 loop set_level(8); set_level(10); end loop;
settle;
check(transitions = base, "30 jitters inside the band: output never moves");
set_level(16); check(allow = '0', "full: stopped");
set_level(0); check(allow = '1', "empty: accepting");
set_level(16);
check(allow = '0', "stopped before reset");
wait until falling_edge(clk); rst_n <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
check(allow = '1', "reset returns to accepting");
wait until falling_edge(clk); rst_n <= '1';
wait until falling_edge(clk);
check(polarity_bad = 0, "rts_n_o was the inverse of allow, every cycle");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then report " RESULT: ALL VHDL FLOW-CTRL TESTS PASSED";
else report " RESULT: VHDL FLOW-CTRL TESTS FAILED" severity error; end if;
done <= '1'; finish;
end process stim;
end architecture sim;The transmit-side testbench measures the synchroniser latency rather than assuming it, and asserts the property §7 is about:
//===========================================================================
// tb_uart_tx_gate_v — self-checking Verilog-2001 testbench
//
// Two properties, and the second is the one engineers get wrong:
// 1. cts_n_i is ASYNCHRONOUS and must pass through two flops, so a change
// takes effect two clocks later -- never on the cycle it arrives.
// 2. tx_busy_i must NOT gate the launch decision. A frame already in
// flight finishes regardless of CTS; gating on busy would truncate it.
//
// INDEPENDENCE: the checker asserts the latency and the truth table from
// the CONTRACT, never by re-deriving the DUT's expression.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_tx_gate_v;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg cts_n = 1'b1, tx_busy = 1'b0, fifo_empty = 1'b1;
wire launch;
uart_tx_gate_v dut (
.clk(clk), .rst_n(rst_n), .cts_n_i(cts_n),
.tx_busy_i(tx_busy), .tx_fifo_empty_i(fifo_empty),
.launch_allowed_o(launch));
integer checks = 0, failures = 0;
task check;
input cond;
input [8*58-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
integer i, lat;
initial begin
#300_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG TX-GATE TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_tx_gate_v : self-checking Verilog testbench ==");
rst_n = 1'b0; cts_n = 1'b0; fifo_empty = 1'b0;
repeat (4) @(negedge clk);
// The synchroniser resets to "not permitted". Coming out of reset
// believing the far end is ready would launch a byte into a device
// that never granted permission.
check(launch === 1'b0, "reset: launch NOT allowed, whatever cts says");
@(negedge clk) rst_n = 1'b1;
// ---- the two-flop latency, measured -------------------------------
// Start from a SETTLED blocked state, then grant permission and
// count active edges until it arrives. Measuring from an unsettled
// start measures the reset sequence, not the synchroniser.
@(negedge clk) cts_n = 1'b1; fifo_empty = 1'b0;
repeat (4) @(negedge clk);
check(launch === 1'b0, "settled blocked before granting permission");
@(negedge clk) cts_n = 1'b0; // permission granted here
lat = 0;
while (launch !== 1'b1 && lat < 10) begin
@(posedge clk); #1;
lat = lat + 1;
end
check(lat == 2, "cts low takes exactly TWO clocks to take effect");
check(launch === 1'b1, "and then the launch is allowed");
// ---- the truth table ----------------------------------------------
@(negedge clk) cts_n = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "cts high: launch blocked");
@(negedge clk) cts_n = 1'b0;
repeat (3) @(negedge clk);
check(launch === 1'b1, "cts low, fifo non-empty: launch allowed");
@(negedge clk) fifo_empty = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "fifo empty: nothing to launch");
@(negedge clk) fifo_empty = 1'b0;
repeat (3) @(negedge clk);
check(launch === 1'b1, "fifo refilled: allowed again");
// ---- THE property: tx_busy must not gate the decision -------------
@(negedge clk) tx_busy = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b1, "tx_busy does NOT block: a frame in flight finishes");
@(negedge clk) tx_busy = 1'b0;
repeat (2) @(negedge clk);
// ---- a glitch narrower than the synchroniser ----------------------
// One clock of permission is not enough to reach the output, which
// is the synchroniser doing its job rather than a defect.
@(negedge clk) cts_n = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "blocked before the glitch");
@(negedge clk) cts_n = 1'b0;
@(negedge clk) cts_n = 1'b1; // one clock wide
repeat (3) @(negedge clk);
check(launch === 1'b0, "a one-clock cts pulse never reaches the output");
// ---- sustained permission does ------------------------------------
@(negedge clk) cts_n = 1'b0;
repeat (4) @(negedge clk);
check(launch === 1'b1, "sustained permission does reach the output");
// ---- reset while permitted ----------------------------------------
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(launch === 1'b0, "reset revokes permission immediately");
@(negedge clk) rst_n = 1'b1;
repeat (4) @(negedge clk);
check(launch === 1'b1, "and permission is re-acquired after two clocks");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG TX-GATE TESTS PASSED");
else $display(" RESULT: VERILOG TX-GATE TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_tx_gate_sv — self-checking Verilog-2001 testbench
//
// Two properties, and the second is the one engineers get wrong:
// 1. cts_n_i is ASYNCHRONOUS and must pass through two flops, so a change
// takes effect two clocks later -- never on the cycle it arrives.
// 2. tx_busy_i must NOT gate the launch decision. A frame already in
// flight finishes regardless of CTS; gating on busy would truncate it.
//
// INDEPENDENCE: the checker asserts the latency and the truth table from
// the CONTRACT, never by re-deriving the DUT's expression.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_tx_gate_sv;
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
logic cts_n = 1'b1, tx_busy = 1'b0, fifo_empty = 1'b1;
logic launch;
uart_tx_gate dut (
.clk(clk), .rst_n(rst_n), .cts_n_i(cts_n),
.tx_busy_i(tx_busy), .tx_fifo_empty_i(fifo_empty),
.launch_allowed_o(launch));
int unsigned checks = 0, failures = 0;
task automatic check(input bit cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
int i, lat;
initial begin
#300us;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG TX-GATE TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_tx_gate : self-checking Verilog testbench ==");
rst_n = 1'b0; cts_n = 1'b0; fifo_empty = 1'b0;
repeat (4) @(negedge clk);
// The synchroniser resets to "not permitted". Coming out of reset
// believing the far end is ready would launch a byte into a device
// that never granted permission.
check(launch === 1'b0, "reset: launch NOT allowed, whatever cts says");
@(negedge clk) rst_n = 1'b1;
// ---- the two-flop latency, measured -------------------------------
// Start from a SETTLED blocked state, then grant permission and
// count active edges until it arrives. Measuring from an unsettled
// start measures the reset sequence, not the synchroniser.
@(negedge clk) cts_n = 1'b1; fifo_empty = 1'b0;
repeat (4) @(negedge clk);
check(launch === 1'b0, "settled blocked before granting permission");
@(negedge clk) cts_n = 1'b0; // permission granted here
lat = 0;
while (launch !== 1'b1 && lat < 10) begin
@(posedge clk); #1;
lat = lat + 1;
end
check(lat == 2, "cts low takes exactly TWO clocks to take effect");
check(launch === 1'b1, "and then the launch is allowed");
// ---- the truth table ----------------------------------------------
@(negedge clk) cts_n = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "cts high: launch blocked");
@(negedge clk) cts_n = 1'b0;
repeat (3) @(negedge clk);
check(launch === 1'b1, "cts low, fifo non-empty: launch allowed");
@(negedge clk) fifo_empty = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "fifo empty: nothing to launch");
@(negedge clk) fifo_empty = 1'b0;
repeat (3) @(negedge clk);
check(launch === 1'b1, "fifo refilled: allowed again");
// ---- THE property: tx_busy must not gate the decision -------------
@(negedge clk) tx_busy = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b1, "tx_busy does NOT block: a frame in flight finishes");
@(negedge clk) tx_busy = 1'b0;
repeat (2) @(negedge clk);
// ---- a glitch narrower than the synchroniser ----------------------
// One clock of permission is not enough to reach the output, which
// is the synchroniser doing its job rather than a defect.
@(negedge clk) cts_n = 1'b1;
repeat (3) @(negedge clk);
check(launch === 1'b0, "blocked before the glitch");
@(negedge clk) cts_n = 1'b0;
@(negedge clk) cts_n = 1'b1; // one clock wide
repeat (3) @(negedge clk);
check(launch === 1'b0, "a one-clock cts pulse never reaches the output");
// ---- sustained permission does ------------------------------------
@(negedge clk) cts_n = 1'b0;
repeat (4) @(negedge clk);
check(launch === 1'b1, "sustained permission does reach the output");
// ---- reset while permitted ----------------------------------------
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(launch === 1'b0, "reset revokes permission immediately");
@(negedge clk) rst_n = 1'b1;
repeat (4) @(negedge clk);
check(launch === 1'b1, "and permission is re-acquired after two clocks");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG TX-GATE TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG TX-GATE TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_tx_gate -- self-checking VHDL-2008 testbench
-- Same 14 checks. The two properties: a two-flop synchroniser latency, and
-- tx_busy NOT gating the launch decision.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use std.env.finish;
entity tb_uart_tx_gate is
end entity tb_uart_tx_gate;
architecture sim of tb_uart_tx_gate is
constant CLK_PERIOD : time := 10 ns;
signal clk, rst_n, done : std_logic := '0';
signal cts_n : std_logic := '1';
signal tx_busy : std_logic := '0';
signal fifo_empty: std_logic := '1';
signal launch : std_logic;
begin
clk <= not clk after CLK_PERIOD/2 when done = '0' else '0';
dut : entity work.uart_tx_gate
port map (clk => clk, rst_n => rst_n, cts_n_i => cts_n,
tx_busy_i => tx_busy, tx_fifo_empty_i => fifo_empty,
launch_allowed_o => launch);
watchdog : process
begin
wait for 300 us;
report " RESULT: VHDL TX-GATE TESTS FAILED (timeout)" severity failure;
wait;
end process watchdog;
stim : process
variable checks, failures, lat : natural := 0;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name;
else failures := failures + 1; report " FAIL " & name severity error; end if;
end procedure;
begin
report "== uart_tx_gate (VHDL) : self-checking testbench ==";
rst_n <= '0'; cts_n <= '0'; fifo_empty <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(launch = '0', "reset: launch NOT allowed, whatever cts says");
wait until falling_edge(clk); rst_n <= '1';
-- start from a SETTLED blocked state before measuring latency
wait until falling_edge(clk); cts_n <= '1'; fifo_empty <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(launch = '0', "settled blocked before granting permission");
wait until falling_edge(clk); cts_n <= '0';
lat := 0;
while launch /= '1' and lat < 10 loop
wait until rising_edge(clk);
wait for 1 ns;
lat := lat + 1;
end loop;
check(lat = 2, "cts low takes exactly TWO clocks to take effect");
check(launch = '1', "and then the launch is allowed");
wait until falling_edge(clk); cts_n <= '1';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '0', "cts high: launch blocked");
wait until falling_edge(clk); cts_n <= '0';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '1', "cts low, fifo non-empty: launch allowed");
wait until falling_edge(clk); fifo_empty <= '1';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '0', "fifo empty: nothing to launch");
wait until falling_edge(clk); fifo_empty <= '0';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '1', "fifo refilled: allowed again");
wait until falling_edge(clk); tx_busy <= '1';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '1', "tx_busy does NOT block: a frame in flight finishes");
wait until falling_edge(clk); tx_busy <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
wait until falling_edge(clk); cts_n <= '1';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '0', "blocked before the glitch");
wait until falling_edge(clk); cts_n <= '0';
wait until falling_edge(clk); cts_n <= '1';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
check(launch = '0', "a one-clock cts pulse never reaches the output");
wait until falling_edge(clk); cts_n <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(launch = '1', "sustained permission does reach the output");
wait until falling_edge(clk); rst_n <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
check(launch = '0', "reset revokes permission immediately");
wait until falling_edge(clk); rst_n <= '1';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(launch = '1', "and permission is re-acquired after two clocks");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then report " RESULT: ALL VHDL TX-GATE TESTS PASSED";
else report " RESULT: VHDL TX-GATE TESTS FAILED" severity error; end if;
done <= '1'; finish;
end process stim;
end architecture sim;== uart_flow_ctrl_v : self-checking Verilog testbench ==
PASS level 8 rising: INSIDE the band, holds
PASS level 12: AT stop threshold, stops
PASS level 8 falling: SAME level, opposite decision
PASS level 6: AT resume threshold, resumes
PASS 20 crossings of STOP produce NO extra transitions
PASS 20 crossings of RESUME produce NO extra transitions
PASS 30 jitters inside the band: output never moves
PASS rts_n_o was the inverse of allow, every cycle
== 21 checks, 0 failures ==
== uart_tx_gate_v : self-checking Verilog testbench ==
PASS reset: launch NOT allowed, whatever cts says
PASS cts low takes exactly TWO clocks to take effect
PASS tx_busy does NOT block: a frame in flight finishes
PASS a one-clock cts pulse never reaches the output
PASS reset revokes permission immediately
== 14 checks, 0 failures ==21 and 14 checks in each language, 0 failures.
Proving these checks can fail
| Mutation | What it breaks | Failures |
|---|---|---|
| ONE threshold instead of two | §5's hysteresis | 4 / 21 |
| one synchroniser flop instead of two | the CDC contract | 1 / 14 |
tx_busy_i added to the launch term | §7's in-flight rule | 1 / 14 |
Both transmit-side mutations are caught by exactly one check, and that is
the point of writing them. A single-flop synchroniser behaves identically in
simulation except for one clock of latency — which is why the latency is
measured rather than eyeballed. Gating on tx_busy_i is invisible
until a frame is actually in flight when CTS deasserts, which is the one
moment §7 says must not truncate a frame.
10. Verification
The suite tests the decision logic and the gate, with a positive control on both sides of every threshold.
| Stimulus | Expected | Result |
|---|---|---|
| reset, low occupancy | permission granted, rts_n_o = 0 | pass |
| level 11 (one below stop) | still permitted | pass |
level 12 (== STOP_LEVEL) | stopped, rts_n_o = 1 | pass |
| level 11, then 7 (between thresholds) | still stopped — held | pass |
level 6 (== RESUME_LEVEL) | permitted again | pass |
| oscillate 11↔12 twenty times | zero toggles on the pin | pass |
| genuine drain to 6 | exactly one toggle | pass |
cts_n_i denied, data waiting | no launch | pass |
cts_n_i permitted, data waiting | launch allowed | pass |
cts_n_i permitted, FIFO empty | no launch | pass |
cts_n_i withdrawn while tx_busy high | gate blocks next launch only | pass |
The chatter test is the one that distinguishes hysteresis from a comment claiming hysteresis. Twenty oscillations across the stop boundary producing zero pin transitions, immediately followed by a genuine drain producing exactly one, tests both that the hold works and that it has not frozen.
// Assertion — permission is withdrawn at or above the stop level and is
// never granted there. States the contract in the direction that matters.
property p_stopped_at_or_above_threshold;
@(posedge clk) disable iff (!rst_n)
(level_i >= CNT_W'(STOP_LEVEL)) |=> !allow_remote_o;
endproperty
assert property (p_stopped_at_or_above_threshold);
// Assertion — permission is restored at or below the resume level.
property p_allowed_at_or_below_resume;
@(posedge clk) disable iff (!rst_n)
(level_i <= CNT_W'(RESUME_LEVEL)) |=> allow_remote_o;
endproperty
assert property (p_allowed_at_or_below_resume);
// Assertion — the hysteresis band HOLDS. Between the two thresholds the
// decision may not change; this is what the chatter test exercises.
property p_hysteresis_holds;
@(posedge clk) disable iff (!rst_n)
((level_i > CNT_W'(RESUME_LEVEL)) && (level_i < CNT_W'(STOP_LEVEL)))
|=> $stable(allow_remote_o);
endproperty
assert property (p_hysteresis_holds);
// Assertion — the two outputs are complements. Cheap, and it catches a
// polarity inversion introduced during integration.
property p_polarity_consistent;
@(posedge clk) disable iff (!rst_n) rts_n_o == ~allow_remote_o;
endproperty
assert property (p_polarity_consistent);
// Assertion — no launch without permission, regardless of pending data.
property p_no_launch_without_cts;
@(posedge clk) disable iff (!rst_n) launch_allowed_o |-> !cts_sync_q;
endproperty
assert property (p_no_launch_without_cts);What is deliberately not asserted: that the far end stops within any particular time. That is a property of the far end, not of this design, and asserting it would be asserting something the architecture does not control — the discipline Chapter 5.4 §7 applied to voting.
The system-level test is different from all of these. Drive characters at the line rate into a FIFO with a deliberately stalled consumer, assert backpressure at the threshold, then count how many characters arrive afterwards and compare against §3's budget. That tests the threshold rather than the comparator, and it is the only test that would catch a headroom that is too small.
11. Debugging
12. What This Means on an FPGA
The logic is one comparator pair and a flip-flop. Hysteresis costs a second comparator; the whole block is a handful of LUTs next to the memory it protects.
cts_n_i needs the same constraint treatment as rx_i. It is a genuine asynchronous input, and the two-stage synchroniser is the only structure in this chapter that timing analysis cares about. Module 12 owns what the constraint says.
Pin polarity is a board-level decision that reaches into the RTL. Some transceivers invert; some connectors cross the pair and some do not. Keeping the polarity in one named assignment — assign rts_n_o = ~allow_q; — means a board change is one line rather than a search.
Probe rts_n_o with level and the arriving characters. The three together answer the only question that matters: did backpressure assert at the right occupancy, and how many bytes arrived after it. That measurement is §3's budget, taken rather than assumed.
Consider whether flow control is reachable at all at the intended rate. §3's 3 Mbaud row is the check worth doing before committing to a depth: if the in-flight budget exceeds the FIFO, the pins are present and useless.
13. Understanding Check
14. Summary
Names are ambiguous, so the convention is stated once: rts_n_o is this device's active-low backpressure output, cts_n_i is its permission input, and in a peer connection they cross.
Flow control has a latency, and two of its terms cannot be engineered away from this side: the frame in progress and a frame already committed — the second being a direct consequence of the gap-free transmission Chapter 7.4 built.
in_flight = 1 + 1 + ceil(recognition_delay / T_frame)2 bytes at 115,200 with a hardware far end; 17 at 3 Mbaud with a 50 µs reaction — where a 16-entry FIFO cannot hold them at any threshold, making it a capacity problem rather than a tuning one.
The threshold is H = DEPTH − STOP_LEVEL ≥ in_flight + margin. At 16 entries and 115,200 baud, stopping at 12 gives 4 bytes of headroom against 2 in flight.
The flow-control threshold and the interrupt threshold are different questions — bytes versus time — and coincide only at particular operating points.
Hysteresis is not optional on a pin. Chatter costs a round trip per transition and presents as throughput far below the configured rate with no errors. Verified: twenty oscillations across the boundary, zero pin transitions; a genuine drain, exactly one.
A frame in progress is never truncated. tx_busy_i is deliberately absent from the launch expression, because freezing the line mid-frame corrupts the character rather than pausing it.
And cts_n_i is the design's second asynchronous input, synchronised like rx_i and reset to not permitted.
15. What Comes Next
Hardware flow control needs two more pins. Plenty of links do not have them — a three-wire connection, a bridge that exposes only data lines, a connector chosen before flow control was considered.
Chapter 10.6 takes the alternative: encode the same request inside the byte stream. That removes the pin requirement and introduces a problem hardware flow control does not have — the control characters occupy values that arbitrary binary payload may legitimately contain, so the mechanism is only transparent if a layer above says it is. It also has its own latency, and the comparison between the two is a genuine trade rather than a ranking.
Browse the full path on the UART tutorials index. For the transmitter launch point this chapter gates, read back to Chapter 7.4.
Continue learning
Related tutorials
- Related topic
Thresholds, Watermarks and Trigger Levels
Full and empty are boundaries, not warnings. A trigger level is chosen by comparing the headroom it leaves against the consumer's worst-case latency — and against the interrupt load it costs.
- Related topic
Overflow, Overrun and Backpressure
At the boundary the buffer is full and the line does not stop. Two events describe the same loss at different points, and the ready signal that backpressures a local producer has no path to the far end.
- Related topic
XON/XOFF Software Flow Control and Its Limits
Sending the stop request inside the data stream removes the pin requirement and creates a problem hardware flow control does not have: the control values are ones arbitrary binary payload may legitimately contain.
- Related topic
Credits — Backpressure Across a Wire You Cannot Reach
An on-chip receiver says no with one wire. A receiver on the far side of a Link cannot, so PCIe replaces the ready signal with advertised capacity and local accounting — six pools, two units, and one rule about spending what you have not been given.
Where this fits
Part of the UART curriculum.
