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. 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 §8 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.
8. Debugging
9. 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.
10. Understanding Check
11. 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.
12. 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.
13. 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.
