UART · Module 10
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.
Chapter 10.5 reached the far end with a dedicated wire. Plenty of links do not have one — a three-wire connection, a bridge exposing only data lines, a connector chosen before flow control was considered.
The alternative is to send the request inside the byte stream that already exists. Two reserved values, one meaning stop and one meaning resume, travelling as ordinary characters.
That removes the pin requirement entirely, and it introduces a problem hardware flow control does not have:
The control values are byte values, and arbitrary binary payload may legitimately contain them.
A mechanism that steals two values out of 256 is transparent only if something above the UART guarantees those values will never appear as data. §4 quantifies how often that guarantee is violated in practice, and it is more often than the framing suggests.
1. The Mechanism
Two ASCII device-control characters, conventionally:
| Value | Name | Meaning |
|---|---|---|
0x13 | DC3, XOFF | stop sending |
0x11 | DC1, XON | resume sending |
Neither is defined by UART. Chapter 3.5 established that a UART frame carries an opaque payload; nothing in the framing distinguishes a control character from data. The assignment above is an agreement between the two endpoints, and a link where only one end has been told will behave in a way that looks like corruption.
The exchange itself is simple:
receiver's buffer fills past its threshold
-> it TRANSMITS 0x13 on its own transmit line
-> the far end's software or driver recognises it
-> the far end stops sending payload
receiver's buffer drains past its resume threshold
-> it TRANSMITS 0x11
-> the far end resumesNote where the request travels: on the receiver's transmit line. Software flow control therefore requires the link to be bidirectional and requires the local transmitter to be available — a point §5 returns to, because it interacts badly with a transmitter that is itself paused.
2. The Detector
// Synthesizable SystemVerilog — the receive-side detector. It watches
// completed characters and pauses the local transmitter.
module uart_xon_xoff #(
parameter logic [7:0] XON = 8'h11, // DC1
parameter logic [7:0] XOFF = 8'h13 // DC3
) (
input logic clk,
input logic rst_n,
input logic rx_char_valid_i,
input logic [7:0] rx_char_data_i,
input logic sw_flow_en_i, // only interpret when ENABLED
output logic tx_paused_o,
output logic consume_char_o // this byte was control, not payload
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_paused_o <= 1'b0;
consume_char_o <= 1'b0;
end else begin
consume_char_o <= 1'b0;
if (sw_flow_en_i && rx_char_valid_i) begin
if (rx_char_data_i == XOFF) begin
tx_paused_o <= 1'b1;
consume_char_o <= 1'b1;
end else if (rx_char_data_i == XON) begin
tx_paused_o <= 1'b0;
consume_char_o <= 1'b1;
end
end
end
end
endmoduleThree details carry the chapter's argument.
sw_flow_en_i gates everything. With software flow control disabled the detector is inert and 0x13 is an ordinary byte. That input is not a convenience — it is the mechanism by which a system carrying binary data avoids §4's problem entirely, and it is why the enable belongs in hardware rather than being assumed always-on.
consume_char_o is separate from tx_paused_o. The first says this byte was control, do not deliver it as payload; the second says stop transmitting. They are different facts and a design that merges them cannot express the case where a control character is recognised but the consumer still wants to see it — which is exactly what a protocol analyser or a transparent bridge needs.
The pause output gates the transmitter's launch, and it composes with Chapter 10.5's gate rather than replacing it:
// Conceptual SystemVerilog — the two mechanisms combine as an AND of
// permissions. Either can withhold; neither overrides the other.
assign launch_allowed = !cts_sync_q // hardware, Chapter 10.5
&& !tx_paused_q // software, this chapter
&& !tx_fifo_empty;And the same rule as Chapter 10.5 §7 applies: this gates the launch, not a frame in progress. Receiving XOFF halfway through transmitting a character does not truncate it — freezing the line mid-frame corrupts the byte rather than pausing it, for the reason that chapter derived.
3. The Latency Is Worse, and for an Extra Reason
Chapter 10.5 §3 built the in-flight budget. Every term in it still applies, and in-band control adds one more before any of them:
HARDWARE: decide -> drive pin -> far end recognises -> in-flight bytes arrive
SOFTWARE: decide -> QUEUE the control byte -> TRANSMIT it (a full frame time)
-> far end receives and recognises -> in-flight bytes arrive
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
this stage does not exist for a pinThe control byte must itself be serialised, which costs a full frame time at minimum — and more if the local transmit FIFO already holds payload:
| baud | T_frame | XOFF sent immediately | queued behind 1 byte | behind 4 | behind 16 |
|---|---|---|---|---|---|
| 9,600 | 1041.67 µs | 1041.7 µs | 2083.3 µs | 5208.3 µs | 17708.3 µs |
| 115,200 | 86.81 µs | 86.8 µs | 173.6 µs | 434.0 µs | 1475.7 µs |
| 1,000,000 | 10.00 µs | 10.0 µs | 20.0 µs | 50.0 µs | 170.0 µs |
4. The Transparency Problem, Quantified
0x11 and 0x13 are two of 256 possible byte values, and payload that is genuinely binary will contain them.
The probability is not small. For uniformly distributed bytes:
P(a given byte is a control value) = 2/256 = 0.781 %| bytes transmitted | P(at least one collision) |
|---|---|
| 100 | 54.36 % |
| 1,000 | 99.96 % |
| 10,000 | 100.00 % |
A hundred bytes of binary data is more likely than not to contain a control value. A firmware image, a sensor record, a compressed frame, a struct with a length field — any of these will trip it within the first packet.
What happens when it does depends on which value collides:
A payload 0x13 is consumed as XOFF: the byte disappears from the data stream and the far end's transmitter stops. The receiver sees corrupted data and the link appears to hang.
A payload 0x11 is consumed as XON: the byte disappears and a pause that was genuinely requested is lifted, so the buffer overflows.
Both are silent. Nothing in the UART layer reports a control character being consumed, because from its point of view nothing went wrong.
5. Two Failure Modes With No Hardware Counterpart
A lost control character deadlocks the link. XOFF is an ordinary frame and can be destroyed by a framing error, a parity error or an overrun on the far end's receiver. If XOFF is lost, the far end never stops and the buffer overflows. If XON is lost, the far end never resumes — and the link is silent indefinitely, with no error reported anywhere, because from the UART's point of view nothing failed.
A pin has no equivalent failure. rts_n_o is a level, continuously asserted; there is no event to miss. That difference is structural: in-band control is edge-like and stateful, out-of-band control is level-like and stateless, and the recovery properties follow directly. A robust software-flow-control implementation usually needs a timeout that re-sends XON, which is a policy decision belonging to the driver.
Deadlock through mutual pause is possible. If both ends pause each other, neither can transmit the XON that would release the other — because the resume character travels on a transmit line that is itself paused. Implementations normally resolve this by exempting control characters from the pause, which is a sensible rule and another thing the hardware form does not need.
6. The Comparison, Without a Winner
| RTS/CTS | XON/XOFF | |
|---|---|---|
| Extra pins | 2 | none |
| Works on a 3-wire link | no | yes |
| Payload transparency | complete | requires a higher-layer guarantee |
| Consumes link bandwidth | no | yes — one frame per transition |
| Reaction path | pin, one clock | queue + serialise + recognise |
| Latency depends on TX traffic | no | yes, unless priority-injected |
| Control can be lost | no — it is a level | yes — deadlock on a lost XON |
| Needs a timeout for robustness | no | usually |
| Implementation | comparator + flip-flop | detector + injection path + policy |
Neither dominates. Hardware flow control is the better mechanism where the pins exist, and the reason is narrower than "it is better engineered": it requires no promise from any layer above it. In-band control requires a guarantee about payload content that the UART cannot verify and did not make.
In-band control is the right choice where pins are genuinely unavailable and the payload is genuinely textual — which describes a large number of real links, including most of the terminal and AT-command traffic the mechanism was designed for. Dismissing it as obsolete misreads why it exists.
What is not defensible is enabling it on a binary link because it was the default, which §4 shows fails within the first hundred bytes.
7. The Same Detector in Three Languages
//===========================================================================
// uart_xon_xoff_v — Synthesizable Verilog-2001
//
// The receive-side detector. It watches COMPLETED characters, which is why
// it sits after the receiver rather than on the wire: XOFF is a byte in the
// data stream, and the only way to recognise it is to have decoded it.
//
// consume_char_o says "this byte was control, not payload" -- the flag that
// keeps a control character out of the application's data.
//===========================================================================
module uart_xon_xoff_v #(
parameter [7:0] XON = 8'h11, // DC1
parameter [7:0] XOFF = 8'h13 // DC3
) (
input wire clk,
input wire rst_n,
input wire rx_char_valid_i,
input wire [7:0] rx_char_data_i,
input wire sw_flow_en_i, // only interpret when ENABLED
output reg tx_paused_o,
output reg consume_char_o
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_paused_o <= 1'b0;
consume_char_o <= 1'b0;
end else begin
consume_char_o <= 1'b0; // default: one-cycle pulse
if (sw_flow_en_i && rx_char_valid_i) begin
if (rx_char_data_i == XOFF) begin
tx_paused_o <= 1'b1;
consume_char_o <= 1'b1;
end else if (rx_char_data_i == XON) begin
tx_paused_o <= 1'b0;
consume_char_o <= 1'b1;
end
end
end
end
endmodule--==========================================================================
-- uart_xon_xoff -- Synthesizable VHDL-2008
--
-- The receive-side detector. It watches COMPLETED characters, which is why
-- it sits after the receiver rather than on the wire: XOFF is a byte in the
-- data stream and can only be recognised once decoded.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
entity uart_xon_xoff is
generic (
XON : std_logic_vector(7 downto 0) := x"11"; -- DC1
XOFF : std_logic_vector(7 downto 0) := x"13" -- DC3
);
port (
clk : in std_logic;
rst_n : in std_logic;
rx_char_valid_i : in std_logic;
rx_char_data_i : in std_logic_vector(7 downto 0);
sw_flow_en_i : in std_logic; -- only interpret when ENABLED
tx_paused_o : out std_logic;
consume_char_o : out std_logic -- control byte, not payload
);
end entity uart_xon_xoff;
architecture rtl of uart_xon_xoff is
begin
process (clk, rst_n)
begin
if rst_n = '0' then
tx_paused_o <= '0';
consume_char_o <= '0';
elsif rising_edge(clk) then
consume_char_o <= '0'; -- default: one-cycle pulse
if sw_flow_en_i = '1' and rx_char_valid_i = '1' then
if rx_char_data_i = XOFF then
tx_paused_o <= '1';
consume_char_o <= '1';
elsif rx_char_data_i = XON then
tx_paused_o <= '0';
consume_char_o <= '1';
end if;
end if;
end if;
end process;
end architecture rtl;| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| control codes | parameter [7:0] | parameter logic [7:0] | generic std_logic_vector | yes |
| watches | completed characters | same | same | yes |
tx_paused_o | LEVEL, held until XON or reset | same | same | yes |
consume_char_o | ONE cycle | one cycle | one cycle | yes |
gated by sw_flow_en_i | yes | yes | yes | yes |
| disabling while paused | does not resume | same | same | yes |
The last row is a contract, not an accident. The pause is state, not a live decode of the current byte. Turning the feature off while the transmitter is paused leaves it paused, because the far end asked for a stop and has not withdrawn it. Only an XON or a reset resumes. A design that resumed on disable would restart a stream the far end is still not ready for.
8. The Self-Checking Testbenches
The block's whole difficulty is that a control character is indistinguishable from payload that happens to have the same value, so the testbenches cover both readings of the identical byte:
// enabled: 0x13 is a command
rx_char(XOFF); check(paused === 1'b1, "XOFF pauses the local transmitter");
// disabled: the SAME byte is data and must pass through untouched
sw_en = 1'b0;
rx_char(XOFF); check(n_consume == base, "disabled: XOFF/XON are payload");That pair is §4's transparency problem stated as a test. The feature does not merely cost two values from the data space — it makes the meaning of a byte depend on a configuration bit the receiver had better agree about.
//===========================================================================
// tb_uart_xon_xoff_v — self-checking Verilog-2001 testbench
//
// The block's whole difficulty is that a control character is INDISTIN-
// GUISHABLE from payload that happens to have the same value. The tests
// therefore cover both readings: with software flow control enabled the
// byte is a command and is consumed; with it disabled the identical byte is
// data and must pass through untouched.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_xon_xoff_v;
localparam [7:0] XON = 8'h11, XOFF = 8'h13;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg rx_valid = 1'b0, sw_en = 1'b1;
reg [7:0] rx_data = 8'h00;
wire paused, consume;
uart_xon_xoff_v #(.XON(XON), .XOFF(XOFF)) dut (
.clk(clk), .rst_n(rst_n),
.rx_char_valid_i(rx_valid), .rx_char_data_i(rx_data),
.sw_flow_en_i(sw_en),
.tx_paused_o(paused), .consume_char_o(consume));
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 n_consume = 0, consume_wide = 0;
reg consume_prev = 1'b0;
always @(posedge clk) if (rst_n) begin
if (consume) begin
n_consume = n_consume + 1;
if (consume_prev) consume_wide = consume_wide + 1;
end
consume_prev = consume;
end
task rx_char;
input [7:0] d;
begin
@(negedge clk) rx_data = d; rx_valid = 1'b1;
@(negedge clk) rx_valid = 1'b0;
@(negedge clk);
end
endtask
task settle; begin repeat (2) @(negedge clk); end endtask
integer base, i;
initial begin
#300_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG XON/XOFF TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_xon_xoff_v : self-checking Verilog testbench ==");
rst_n = 1'b0; sw_en = 1'b1;
repeat (4) @(negedge clk);
check(paused === 1'b0 && consume === 1'b0, "reset: not paused, no consume");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- ordinary payload passes through ------------------------------
base = n_consume;
rx_char(8'h41); rx_char(8'hA5); rx_char(8'h00); rx_char(8'hFF);
settle;
check(n_consume == base, "four ordinary bytes: none consumed");
check(paused === 1'b0, "four ordinary bytes: still not paused");
// ---- XOFF pauses ---------------------------------------------------
base = n_consume;
rx_char(XOFF);
settle;
check(paused === 1'b1, "XOFF pauses the local transmitter");
check(n_consume == base + 1, "XOFF is consumed, exactly once");
// ---- the pause persists across unrelated traffic -------------------
rx_char(8'h42); rx_char(8'h43);
check(paused === 1'b1, "pause survives intervening payload");
// ---- a second XOFF is idempotent ------------------------------------
base = n_consume;
rx_char(XOFF);
settle;
check(paused === 1'b1, "second XOFF: still paused");
check(n_consume == base + 1, "second XOFF is still consumed");
// ---- XON resumes ----------------------------------------------------
base = n_consume;
rx_char(XON);
settle;
check(paused === 1'b0, "XON resumes the local transmitter");
check(n_consume == base + 1, "XON is consumed too");
// ---- XON when not paused --------------------------------------------
base = n_consume;
rx_char(XON);
settle;
check(paused === 1'b0, "XON while running: stays running");
check(n_consume == base + 1, "and is still consumed as a control byte");
// ---- THE ambiguity: the same byte with the feature DISABLED ---------
// 0x13 is a legitimate data value. With software flow control off it
// is payload and must not be touched. This is the cost the chapter
// names: the feature steals two values from the data space.
@(negedge clk) sw_en = 1'b0;
base = n_consume;
rx_char(XOFF); rx_char(XON);
settle;
check(n_consume == base, "disabled: XOFF/XON are payload, not consumed");
check(paused === 1'b0, "disabled: transmitter never pauses");
// ---- re-enabling mid-stream ------------------------------------------
@(negedge clk) sw_en = 1'b1;
base = n_consume;
rx_char(XOFF);
settle;
check(paused === 1'b1, "re-enabled: XOFF is a command again");
check(n_consume == base + 1, "re-enabled: and is consumed");
// ---- disabling WHILE paused -------------------------------------------
// The pause is state, not a live decode. Disabling the feature does
// not resume the transmitter -- only an XON or a reset does.
@(negedge clk) sw_en = 1'b0;
repeat (4) @(negedge clk);
check(paused === 1'b1, "disabling while paused does NOT resume");
@(negedge clk) sw_en = 1'b1;
rx_char(XON);
check(paused === 1'b0, "XON still resumes after re-enabling");
// ---- a byte with no valid strobe is not seen --------------------------
base = n_consume;
@(negedge clk) rx_data = XOFF; rx_valid = 1'b0;
repeat (4) @(negedge clk);
check(n_consume == base, "data without rx_char_valid is ignored");
check(paused === 1'b0, "and does not pause");
// ---- reset while paused ------------------------------------------------
rx_char(XOFF);
check(paused === 1'b1, "paused before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(paused === 1'b0, "reset resumes the transmitter");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(consume_wide == 0, "consume pulse never wider than one clock");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG XON/XOFF TESTS PASSED");
else $display(" RESULT: VERILOG XON/XOFF TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_xon_xoff_sv — self-checking Verilog-2001 testbench
//
// The block's whole difficulty is that a control character is INDISTIN-
// GUISHABLE from payload that happens to have the same value. The tests
// therefore cover both readings: with software flow control enabled the
// byte is a command and is consumed; with it disabled the identical byte is
// data and must pass through untouched.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_xon_xoff_sv;
localparam [7:0] XON = 8'h11, XOFF = 8'h13;
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
logic rx_valid = 1'b0, sw_en = 1'b1;
logic [7:0] rx_data = 8'h00;
logic paused, consume;
uart_xon_xoff #(.XON(XON), .XOFF(XOFF)) dut (
.clk(clk), .rst_n(rst_n),
.rx_char_valid_i(rx_valid), .rx_char_data_i(rx_data),
.sw_flow_en_i(sw_en),
.tx_paused_o(paused), .consume_char_o(consume));
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 n_consume = 0, consume_wide = 0;
logic consume_prev = 1'b0;
always @(posedge clk) if (rst_n) begin
if (consume) begin
n_consume = n_consume + 1;
if (consume_prev) consume_wide = consume_wide + 1;
end
consume_prev = consume;
end
task automatic rx_char(input logic [7:0] d);
@(negedge clk) rx_data = d; rx_valid = 1'b1;
@(negedge clk) rx_valid = 1'b0;
@(negedge clk);
endtask
task automatic settle(); repeat (2) @(negedge clk); endtask
int base, i;
initial begin
#300us;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG XON/XOFF TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_xon_xoff : self-checking Verilog testbench ==");
rst_n = 1'b0; sw_en = 1'b1;
repeat (4) @(negedge clk);
check(paused === 1'b0 && consume === 1'b0, "reset: not paused, no consume");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- ordinary payload passes through ------------------------------
base = n_consume;
rx_char(8'h41); rx_char(8'hA5); rx_char(8'h00); rx_char(8'hFF);
settle();
check(n_consume == base, "four ordinary bytes: none consumed");
check(paused === 1'b0, "four ordinary bytes: still not paused");
// ---- XOFF pauses ---------------------------------------------------
base = n_consume;
rx_char(XOFF);
settle();
check(paused === 1'b1, "XOFF pauses the local transmitter");
check(n_consume == base + 1, "XOFF is consumed, exactly once");
// ---- the pause persists across unrelated traffic -------------------
rx_char(8'h42); rx_char(8'h43);
check(paused === 1'b1, "pause survives intervening payload");
// ---- a second XOFF is idempotent ------------------------------------
base = n_consume;
rx_char(XOFF);
settle();
check(paused === 1'b1, "second XOFF: still paused");
check(n_consume == base + 1, "second XOFF is still consumed");
// ---- XON resumes ----------------------------------------------------
base = n_consume;
rx_char(XON);
settle();
check(paused === 1'b0, "XON resumes the local transmitter");
check(n_consume == base + 1, "XON is consumed too");
// ---- XON when not paused --------------------------------------------
base = n_consume;
rx_char(XON);
settle();
check(paused === 1'b0, "XON while running: stays running");
check(n_consume == base + 1, "and is still consumed as a control byte");
// ---- THE ambiguity: the same byte with the feature DISABLED ---------
// 0x13 is a legitimate data value. With software flow control off it
// is payload and must not be touched. This is the cost the chapter
// names: the feature steals two values from the data space.
@(negedge clk) sw_en = 1'b0;
base = n_consume;
rx_char(XOFF); rx_char(XON);
settle();
check(n_consume == base, "disabled: XOFF/XON are payload, not consumed");
check(paused === 1'b0, "disabled: transmitter never pauses");
// ---- re-enabling mid-stream ------------------------------------------
@(negedge clk) sw_en = 1'b1;
base = n_consume;
rx_char(XOFF);
settle();
check(paused === 1'b1, "re-enabled: XOFF is a command again");
check(n_consume == base + 1, "re-enabled: and is consumed");
// ---- disabling WHILE paused -------------------------------------------
// The pause is state, not a live decode. Disabling the feature does
// not resume the transmitter -- only an XON or a reset does.
@(negedge clk) sw_en = 1'b0;
repeat (4) @(negedge clk);
check(paused === 1'b1, "disabling while paused does NOT resume");
@(negedge clk) sw_en = 1'b1;
rx_char(XON);
check(paused === 1'b0, "XON still resumes after re-enabling");
// ---- a byte with no valid strobe is not seen --------------------------
base = n_consume;
@(negedge clk) rx_data = XOFF; rx_valid = 1'b0;
repeat (4) @(negedge clk);
check(n_consume == base, "data without rx_char_valid is ignored");
check(paused === 1'b0, "and does not pause");
// ---- reset while paused ------------------------------------------------
rx_char(XOFF);
check(paused === 1'b1, "paused before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(paused === 1'b0, "reset resumes the transmitter");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
check(consume_wide == 0, "consume pulse never wider than one clock");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG XON/XOFF TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG XON/XOFF TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_xon_xoff -- self-checking VHDL-2008 testbench
-- Same 23 checks. Both readings of the ambiguous byte are covered: enabled
-- it is a command, disabled it is payload.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use std.env.finish;
entity tb_uart_xon_xoff is
end entity tb_uart_xon_xoff;
architecture sim of tb_uart_xon_xoff is
constant XONC : std_logic_vector(7 downto 0) := x"11";
constant XOFFC : std_logic_vector(7 downto 0) := x"13";
constant CLK_PERIOD : time := 10 ns;
signal clk, rst_n, done : std_logic := '0';
signal rx_valid : std_logic := '0';
signal sw_en : std_logic := '1';
signal rx_data : std_logic_vector(7 downto 0) := (others => '0');
signal paused, consume : std_logic;
signal n_consume, consume_wide : natural := 0;
begin
clk <= not clk after CLK_PERIOD/2 when done = '0' else '0';
dut : entity work.uart_xon_xoff
generic map (XON => XONC, XOFF => XOFFC)
port map (clk => clk, rst_n => rst_n,
rx_char_valid_i => rx_valid, rx_char_data_i => rx_data,
sw_flow_en_i => sw_en,
tx_paused_o => paused, consume_char_o => consume);
obs : process (clk)
variable n, w : natural := 0;
variable prev : std_logic := '0';
begin
if rising_edge(clk) and rst_n = '1' then
if consume = '1' then
n := n + 1;
if prev = '1' then w := w + 1; end if;
end if;
prev := consume;
n_consume <= n; consume_wide <= w;
end if;
end process obs;
watchdog : process
begin
wait for 300 us;
report " RESULT: VHDL XON/XOFF 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 rx_char(d : std_logic_vector) is
begin
wait until falling_edge(clk); rx_data <= d; rx_valid <= '1';
wait until falling_edge(clk); rx_valid <= '0';
wait until falling_edge(clk);
end procedure;
procedure settle is
begin
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
end procedure;
begin
report "== uart_xon_xoff (VHDL) : self-checking testbench ==";
rst_n <= '0'; sw_en <= '1';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(paused = '0' and consume = '0', "reset: not paused, no consume");
rst_n <= '1'; wait until falling_edge(clk);
base := n_consume;
rx_char(x"41"); rx_char(x"A5"); rx_char(x"00"); rx_char(x"FF");
settle;
check(n_consume = base, "four ordinary bytes: none consumed");
check(paused = '0', "four ordinary bytes: still not paused");
base := n_consume; rx_char(XOFFC); settle;
check(paused = '1', "XOFF pauses the local transmitter");
check(n_consume = base+1, "XOFF is consumed, exactly once");
rx_char(x"42"); rx_char(x"43");
check(paused = '1', "pause survives intervening payload");
base := n_consume; rx_char(XOFFC); settle;
check(paused = '1', "second XOFF: still paused");
check(n_consume = base+1, "second XOFF is still consumed");
base := n_consume; rx_char(XONC); settle;
check(paused = '0', "XON resumes the local transmitter");
check(n_consume = base+1, "XON is consumed too");
base := n_consume; rx_char(XONC); settle;
check(paused = '0', "XON while running: stays running");
check(n_consume = base+1, "and is still consumed as a control byte");
wait until falling_edge(clk); sw_en <= '0';
base := n_consume; rx_char(XOFFC); rx_char(XONC); settle;
check(n_consume = base, "disabled: XOFF/XON are payload, not consumed");
check(paused = '0', "disabled: transmitter never pauses");
wait until falling_edge(clk); sw_en <= '1';
base := n_consume; rx_char(XOFFC); settle;
check(paused = '1', "re-enabled: XOFF is a command again");
check(n_consume = base+1, "re-enabled: and is consumed");
wait until falling_edge(clk); sw_en <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(paused = '1', "disabling while paused does NOT resume");
wait until falling_edge(clk); sw_en <= '1';
rx_char(XONC);
check(paused = '0', "XON still resumes after re-enabling");
base := n_consume;
wait until falling_edge(clk); rx_data <= XOFFC; rx_valid <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(n_consume = base, "data without rx_char_valid is ignored");
check(paused = '0', "and does not pause");
rx_char(XOFFC);
check(paused = '1', "paused 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(paused = '0', "reset resumes the transmitter");
wait until falling_edge(clk); rst_n <= '1';
wait until falling_edge(clk);
check(consume_wide = 0, "consume pulse never wider than one clock");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then report " RESULT: ALL VHDL XON/XOFF TESTS PASSED";
else report " RESULT: VHDL XON/XOFF TESTS FAILED" severity error; end if;
done <= '1'; finish;
end process stim;
end architecture sim;== uart_xon_xoff_v : self-checking Verilog testbench ==
PASS four ordinary bytes: none consumed
PASS XOFF pauses the local transmitter
PASS XOFF is consumed, exactly once
PASS pause survives intervening payload
PASS second XOFF: still paused
PASS XON resumes the local transmitter
PASS XON while running: stays running
PASS disabled: XOFF/XON are payload, not consumed
PASS re-enabled: XOFF is a command again
PASS disabling while paused does NOT resume
PASS data without rx_char_valid is ignored
PASS reset resumes the transmitter
PASS consume pulse never wider than one clock
== 23 checks, 0 failures ==
RESULT: ALL VERILOG XON/XOFF TESTS PASSED23 checks in each language, 0 failures.
Proving these checks can fail
| Mutation | What it breaks | Failures |
|---|---|---|
consume_char_o is a level, not a pulse | the one-cycle contract | 8 / 23 |
ignores sw_flow_en_i, always interprets | §4's transparency rule | 1 / 23 |
One check out of twenty-three catches the second one, and it is the check that drives 0x13 with the feature disabled. A detector that always interprets is correct for every enabled test and silently eats a data byte the moment a user turns the feature off — which is the shape of a bug that reaches a customer rather than a regression.
9. Verification
| Stimulus | Expected | Result |
|---|---|---|
| reset | not paused | pass |
ordinary byte 0x41 | no pause, not consumed | pass |
0x13 with flow control enabled | paused, consumed | pass |
| ordinary byte while paused | stays paused | pass |
0x11 with flow control enabled | resumed, consumed | pass |
0x13 with flow control DISABLED | no pause, not consumed — it is payload | pass |
The last row is the transparency contract expressed as a test, and it is the one that matters. A detector that acts on 0x13 regardless of the enable has made the mechanism unconditional, and every binary link using that UART will corrupt data — silently, within the first hundred bytes per §4.
The positive control is the ordinary byte. A detector that pauses on everything passes every negative test.
// Assertion — a pause change requires an enabled control character.
// Nothing else may alter the pause state.
property p_pause_needs_control;
@(posedge clk) disable iff (!rst_n)
$changed(tx_paused_o) |-> $past(sw_flow_en_i && rx_char_valid_i);
endproperty
assert property (p_pause_needs_control);
// Assertion — with software flow control disabled, control VALUES are
// ordinary payload. This is the transparency contract.
property p_disabled_is_transparent;
@(posedge clk) disable iff (!rst_n)
!sw_flow_en_i |=> (!consume_char_o && $stable(tx_paused_o));
endproperty
assert property (p_disabled_is_transparent);
// Assertion — a consumed character is always one of the two control values.
property p_consume_only_control;
@(posedge clk) disable iff (!rst_n)
consume_char_o |-> ($past(rx_char_data_i) inside {XON, XOFF});
endproperty
assert property (p_consume_only_control);What is not asserted: that the far end stops within any time bound. That is a property of the far end's software, which this design does not control — the same discipline Chapter 10.5 §9 applied.
The system test that matters is a binary payload sweep with software flow control enabled, asserting that data is corrupted. Writing a test whose pass condition is the mechanism breaks the link feels wrong and pins the boundary exactly — and it documents in executable form why the enable exists.
10. Debugging
11. What This Means on an FPGA
The detector is two comparators and two flip-flops. It is the cheapest block in Module 10, and its cost is entirely in the surrounding policy rather than the logic.
The priority-injection path is where the real work is. Inserting a control character ahead of queued payload means either a separate high-priority register the transmit engine checks before popping the FIFO, or a small bypass — and either way it must not disturb a frame in flight. That is more logic than the detector and it is what makes the mechanism's latency deterministic.
Bring the enable out as a register bit, not a parameter. Whether a link carries text or binary is a runtime property of the system, and a design that fixes it at synthesis will be wrong for one of its use cases. Module 13 owns the register; the decision to make it runtime belongs to whoever instantiates this block.
Probe consume_char_o during bring-up on a binary link. Any assertion at all is §4 occurring, and it is the direct evidence that the enable is set wrongly — far more useful than inferring it from missing bytes.
12. Assertions Across the Module
Five blocks, and the properties worth asserting are the ones whose violation leaves the design working — where only the meaning is wrong.
// Published as SystemVerilog Assertions. Icarus Verilog 13.0 supports
// neither named properties nor $past, so what was EXECUTED is the
// procedural equivalent inside each testbench.
// p_level_bounded -- catches a width or update error in the occupancy
// counter. A level that can exceed DEPTH makes `full` meaningless and a
// level that cannot reach DEPTH makes it unreachable.
property p_level_bounded;
@(posedge clk) disable iff (!rst_n) level_o <= CNT_W'(DEPTH);
endproperty
// p_empty_full_exclusive -- catches the two flags being derived from
// different quantities. They are both functions of one counter and can
// never both be true unless DEPTH is zero.
property p_empty_full_exclusive;
@(posedge clk) disable iff (!rst_n) !(empty_o && full_o);
endproperty
// p_no_overflow_on_handoff -- catches the `push && !full` formulation.
// A push that coincides with a pop must be accepted, and reporting an
// overflow there is a lost byte at exactly the moment the consumer was
// keeping up.
property p_no_overflow_on_handoff;
@(posedge clk) disable iff (!rst_n)
(push_i && pop_i && full_o) |=> !overflow_evt_o;
endproperty
// p_hysteresis_holds -- catches a single-threshold flow controller. Between
// the two levels the decision must not change, whatever occupancy does.
property p_hysteresis_holds;
@(posedge clk) disable iff (!rst_n)
(level_i > RESUME_LEVEL && level_i < STOP_LEVEL) |=> $stable(allow_remote_o);
endproperty
// p_cts_two_flops -- catches a synchroniser shortened to one stage. The
// output must not respond to cts_n_i in fewer than two clocks.
property p_cts_two_flops;
@(posedge clk) disable iff (!rst_n)
$changed(cts_n_i) |=> $stable(launch_allowed_o);
endproperty
// p_consume_is_a_pulse -- catches a control-byte strobe held as a level,
// which makes a downstream consumer discard more than one byte.
property p_consume_is_a_pulse;
@(posedge clk) disable iff (!rst_n) consume_char_o |=> !consume_char_o;
endproperty| Property | Defect it catches | Why it would otherwise be missed |
|---|---|---|
p_level_bounded | occupancy width wrong | full unreachable; the FIFO silently never asserts backpressure |
p_empty_full_exclusive | flags from different quantities | both false simultaneously reads as "partly filled" forever |
p_no_overflow_on_handoff | push && !full | one byte lost per full-FIFO hand-off, under load only |
p_hysteresis_holds | single threshold | RTS chatters; the far end sees a flapping permission |
p_cts_two_flops | one-stage synchroniser | identical behaviour except under metastability |
p_consume_is_a_pulse | strobe held as a level | payload bytes silently eaten after a control byte |
13. UVM Mapping
The full environment is Module 16's subject. What is worth fixing here is which part of the buffering contract each component owns, because those boundaries follow from this module rather than from UVM.
| Contract element | UVM component | What it carries |
|---|---|---|
| a byte to send, and when | sequence item | data, gap_bits (inter-character idle) |
| producing it on the wire | driver | serialisation at the configured baud |
| how fast software drains | a second sequence | service_latency, burst_size |
observing rts_n / cts_n | monitor | pin transitions, timestamped |
| observing delivered bytes | monitor | what software actually read |
| what should have survived | predictor | the queue model, from the stimulus alone |
| comparison | scoreboard | delivered sequence, and which bytes were lost |
Three things in that table are decided by this module, not by UVM.
The interesting stimulus is the CONSUMER, not the producer. Every other UART agent drives the line and watches the DUT. Here the line is the easy half: overflow happens because software was slow, so the sequence that matters models service latency. An environment that only varies the traffic will never fill a FIFO that is being drained promptly, and will report full coverage of a block it never stressed.
The scoreboard must check WHICH bytes were lost, not how many. §3 established the policy — the new byte is dropped, the held one survives — and a scoreboard counting losses would pass the opposite policy. This is not theoretical: the mutation that inverts the policy is caught by three checks in the unit testbench, and all three inspect payload identity.
Flow control needs a two-sided monitor and a latency budget, not a pass/fail. §3 of 10.5 derived the in-flight budget: bytes already committed when RTS deasserts are expected to arrive. A scoreboard that flagged them as overflow would fail a correct design. The check is that arrivals after the deassertion do not exceed the budget.
Coverage worth collecting, from the axes this module established:
| Axis | Bins | Why |
|---|---|---|
| FIFO occupancy at push | empty, 1, mid, DEPTH−1, DEPTH | the boundary is the only interesting point |
| simultaneous push/pop | at empty, mid, at full | §7's enumerated case |
| DEPTH | power of two, non-power-of-two | a truncating pointer is correct at one and not the other |
| service latency | prompt, marginal, absent | overflow only exists in the third |
| flow control | none, RTS/CTS, XON/XOFF | three different failure surfaces |
| occupancy relative to thresholds | below RESUME, in band, above STOP | hysteresis is only testable in the band |
The cross that earns its keep is occupancy × simultaneous-push-pop,
because the acceptance rule of §7 differs from the naive one at exactly one
point in that cross and nowhere else.
What is deliberately not here: no uvm_component boilerplate, no
factory registration, no config-db plumbing. None of it teaches anything
about buffering.
14. The Module 10 Backpressure Matrix
Every way this module's hardware can refuse, lose or delay a byte, end to end. Read the "Where it stops" column first — it is what distinguishes these mechanisms from one another.
| Mechanism | Trigger | Where it stops | Latency to effect | What is lost | Recovery |
|---|---|---|---|---|---|
| FIFO full | occupancy = DEPTH | nothing — the FIFO just refuses | immediate | the new byte (10.4 §3) | automatic once software reads |
| FIFO empty | occupancy = 0 | nothing — the pop just fails | immediate | nothing; underflow is a statement, not a loss | automatic on the next push |
| RX watermark | level ≥ trigger | nothing — it only asks | immediate | nothing | software services the FIFO |
| TX watermark | level ≤ trigger | nothing — it only asks | immediate | nothing | software refills |
| RTS deassert | level ≥ STOP_LEVEL | the far end's transmitter | its detection + in-flight bytes | nothing, if the budget was right | level falls to RESUME_LEVEL |
| CTS deassert | far end's decision | our launch, at a frame boundary | 2 clocks (synchroniser) + current frame | nothing | CTS reasserts |
| XOFF received | a byte equal to 0x13 | our transmitter | one whole character time, minimum | nothing — but 0x13 is no longer usable data | XON received |
Three rows stop nothing at all, and that is the distinction §1 of 10.3 is built on: a watermark is a warning, a full flag is a boundary. Confusing them produces a design that politely announces a condition it has already failed to prevent.
One row has a latency measured in character times rather than clocks. An XOFF cannot be acted on until the whole character carrying it has been received, which is why §3 of this chapter calls its latency worse and for an extra reason.
15. The Module 10 Verification Matrix
| Requirement | Directed test | Corner test | Assertion | Coverage | Waveform |
|---|---|---|---|---|---|
| occupancy 0..DEPTH, never beyond | 1500 random cycles ×3 | fill to DEPTH exactly | p_level_bounded | occupancy bins | 10.2 Fig 1 |
full reachable at all | fill to DEPTH | counter width | p_level_bounded | — | — |
| push+pop at full is accepted | explicit case ×3 | at empty too | p_no_overflow_on_handoff | occupancy × simultaneity | 10.2 §7 |
| overflow drops the new byte | payload identity ×3 | 5 offered, 1 kept | — | — | 10.4 Fig 1 |
| pointers wrap explicitly | DEPTH = 5 twin | 200 wrap cycles | — | DEPTH bins | — |
| byte order preserved | queue model, 1500 cycles | across wrap | — | — | — |
| watermark senses are opposite | empty and full ×3 | 17×17 exhaustive | — | — | 10.3 Fig 1 |
| trigger levels are runtime | reprogram mid-test ×3 | degenerate 0 / DEPTH | — | — | — |
| hysteresis holds in the band | rise-then-fall ×3 | 70 boundary crossings | p_hysteresis_holds | threshold bins | 10.5 Fig 1 |
rts_n is the inverse of allow | every cycle ×3 | — | — | — | — |
| CTS takes two clocks | measured, not assumed | one-clock glitch | p_cts_two_flops | — | — |
| a frame in flight is never truncated | tx_busy held ×3 | — | — | — | 10.5 §7 |
| XOFF/XON only when enabled | both readings ×3 | disable while paused | — | flow-control bins | — |
consume_char_o is one cycle | whole-run observer ×3 | — | p_consume_is_a_pulse | — | — |
| reset clears every block | all five ×3 | reset while full/paused | — | — | — |
Two rows have no assertion and that is deliberate. "Overflow drops the new byte" and "byte order preserved" are statements about a sequence of payloads, which an immediate assertion cannot express; they are checked by the queue model instead, which is the stronger instrument.
16. Module Evidence
Five RTL blocks, each in three languages, each with a self-checking testbench in its own language. Every number quoted anywhere in Module 10 comes from these runs.
| Block | Chapter | Verilog | SystemVerilog | VHDL |
|---|---|---|---|---|
uart_sync_fifo | 10.2 §11 | 30 | 30 | 30 |
uart_fifo_watermark | 10.3 §8 | 16 | 16 | 16 |
uart_flow_ctrl | 10.5 §9 | 21 | 21 | 21 |
uart_tx_gate | 10.5 §9 | 14 | 14 | 14 |
uart_xon_xoff | §8 above | 23 | 23 | 23 |
| total | 104 | 104 | 104 |
312 checks across three languages, 0 failures.
| Language | Tool | Compile | Simulate |
|---|---|---|---|
| Verilog-2001 | Icarus Verilog 13.0 (-g2001) | clean | yes |
| SystemVerilog | Icarus Verilog 13.0 (-g2012) | clean | yes |
| VHDL-2008 | NVC 1.23.0 (LLVM 23.1.1) | clean | yes |
The per-block counts are identical in all three languages. That is the acceptance criterion rather than a coincidence: a module where SystemVerilog runs two hundred checks and VHDL runs three has verified one design and compiled two others.
The whole mutation campaign
Ten deliberate defects, each run against the unmodified testbench for its block:
| Mutation | Block | Failures |
|---|---|---|
| occupancy counter sized like a pointer | FIFO | 19 / 30 |
| push rejected when it coincides with a pop | FIFO | 9 / 30 |
consume_char_o held as a level | XON/XOFF | 8 / 23 |
two conditional increments instead of one case | FIFO | 7 / 30 |
| both watermark senses the same direction | watermark | 6 / 16 |
| one threshold instead of two | flow control | 4 / 21 |
| pointers truncate instead of wrapping | FIFO | 3 / 30 |
| one synchroniser flop instead of two | TX gate | 1 / 14 |
tx_busy_i added to the launch term | TX gate | 1 / 14 |
detector ignores sw_flow_en_i | XON/XOFF | 1 / 23 |
17. Understanding Check
18. Summary
In-band flow control sends the request inside the byte stream: 0x13 for XOFF and 0x11 for XON, by convention rather than by anything UART defines. It removes the two-pin requirement and works on a three-wire link.
The detector is trivial; the surrounding policy is not. sw_flow_en_i gates the interpretation, and consume_char_o is separate from tx_paused_o because they state different facts. The pause gates the launch, never a frame in progress.
The latency has a stage the pin form does not: the control character must be serialised, costing at least one frame time — and 1.48 ms at 115,200 baud if it waits behind sixteen queued payload bytes. The remedy is priority injection, which is more logic than the detector.
The transparency problem is quantitative: two values in 256 means 54% of 100 random bytes contain one, and 99.96% of 1,000. A payload 0x13 stops the far end and vanishes; a payload 0x11 lifts a genuine pause and vanishes. Both silently.
The UART cannot solve it, because the framing carries no type information. The four responses — disable, reserve, escape, or use a framing protocol that already escapes — all live above the UART.
A lost XON deadlocks the link silently, because in-band control is stateful and edge-like where a pin is stateless and level-like. Mutual pause needs control characters exempted.
Neither mechanism dominates. Hardware flow control is preferred where pins allow — not because it is better engineered, but because it requires no promise from any layer above it. In-band control remains right for text-oriented links without spare pins, which is a large fraction of real traffic.
19. Where Module 10 Leaves You
Chapter 10.1 established that depth buys time, not bandwidth — a consumer 0.17% too slow overflows 64 entries in 3.2 seconds. Chapter 10.2 built the FIFO and settled that it is synchronous unless the two sides genuinely sit in different clock domains, fixing a contract that every later chapter used unchanged. Chapter 10.3 derived trigger levels from a latency budget and showed the interrupt-load cost of every entry of headroom. Chapter 10.4 located the boundary precisely and established that ready has no path off the device. Chapter 10.5 reached the far end with a pin and proved the reach is not instantaneous — 17 bytes in flight at 3 Mbaud with a 50 µs far end, which no 16-entry FIFO can absorb. This chapter reached it without a pin, at the cost of a promise nobody may have made.
The thread through all six: buffering converts a hard real-time requirement into an average-rate requirement, and flow control converts an average-rate requirement into a negotiated one. Neither creates capacity. A link whose consumer is genuinely slower than its line fails under all three, and the only remaining levers are a lower rate or a faster consumer.
An engineer holding this module can size a FIFO from a measured latency, derive two different thresholds for two different questions, say which byte is lost and why, compute how many characters arrive after backpressure is asserted, and tell when flow control cannot work at all.
20. What Comes Next
Every block in this module was built and verified independently. That was deliberate — a FIFO whose correctness depends on the transmitter it feeds is a FIFO nobody can reason about.
Module 11 assembles them. One UART IP with a receiver, a transmitter, a baud generator, both FIFOs, the thresholds and the flow control — plus the configuration and status plumbing that lets one coherent interface present all of it. That is where the independent pieces acquire a top level, a configuration story, and the integration decisions that only appear when blocks meet.
After that: Module 12 returns to the clock-domain and reset questions this module deliberately scoped out — including the genuine asynchronous FIFO for the case where the two sides really do sit in different domains; Module 13 builds the register interface, interrupts and DMA hooks that the triggers and status of this module have been produced for.
Browse the full path on the UART tutorials index. For the hardware mechanism this chapter is the alternative to, read back to Chapter 10.5.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
What a UART Actually Is
Two digital systems need to exchange a small amount of data over very few wires, and no clock travels with it. A UART is the logic that answers that problem — it converts between locally meaningful parallel data and timed activity on a single line, and the timing agreement it depends on is what the rest of the curriculum builds.
- Related topic
Synchronous vs Asynchronous Serial Links
A forwarded clock is a sampling reference generated by the same source as the data. Remove it and the receiver must assemble one from a configured rate, an observable event in the signal, and its own local clock — the responsibility shift that turns a receiver into a state machine and shapes every UART design decision that follows.
- Related topic
The UART Link: TX, RX, Idle and Full Duplex
A UART link is two independent one-way conductors, not one bidirectional bus — which removes arbitration, turnaround and direction control from the design, makes the naming endpoint-relative, and means full duplex guarantees simultaneity and nothing else.
Where this fits
Part of the UART curriculum.
