Skip to content
VLSI Mentor

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:

ValueNameMeaning
0x13DC3, XOFFstop sending
0x11DC1, XONresume 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 resumes

Note 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Three 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 pin

The control byte must itself be serialised, which costs a full frame time at minimum — and more if the local transmit FIFO already holds payload:

baudT_frameXOFF sent immediatelyqueued behind 1 bytebehind 4behind 16
9,6001041.67 µs1041.7 µs2083.3 µs5208.3 µs17708.3 µs
115,20086.81 µs86.8 µs173.6 µs434.0 µs1475.7 µs
1,000,00010.00 µs10.0 µs20.0 µs50.0 µs170.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
P(a given byte is a control value)  =  2/256  =  0.781 %
bytes transmittedP(at least one collision)
10054.36 %
1,00099.96 %
10,000100.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.

A sequence showing in-band software flow control. The local receive FIFO's occupancy rises past its stop threshold and the flow control logic decides to request a pause. Because the request travels inside the data stream, the control character must first be queued on the local transmit path and then serialised, which takes at least one full frame time and longer if payload is already queued ahead of it. The character then propagates to the far end, where the far end's receiver assembles it and its software or driver recognises it as a stop request and ceases transmitting payload. Meanwhile the characters the far end had already committed continue to arrive, exactly as they do with hardware flow control. When the local buffer drains, a resume character is queued and serialised in the same way and the far end restarts.RX FIFOLocal TXFar endFar SWlevel >= STOP: queueXOFF (0x13)SERIALISE — >= 1frame time, more ifqueued0x13 on the wireassembled,recognised ascontrolstop launchingpayloadin-flight bytesstill arrive — Ch10.5 §3level <= RESUME:queue XON (0x11)
Figure 1 — the in-band round trip, and where it differs. Time runs downward. Every stage the hardware form has is present, plus one that has no counterpart: the control character must be serialised on the local transmit line before the far end can begin to react, and if the transmit queue is not empty it waits behind whatever is already there.

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/CTSXON/XOFF
Extra pins2none
Works on a 3-wire linknoyes
Payload transparencycompleterequires a higher-layer guarantee
Consumes link bandwidthnoyes — one frame per transition
Reaction pathpin, one clockqueue + serialise + recognise
Latency depends on TX trafficnoyes, unless priority-injected
Control can be lostno — it is a levelyes — deadlock on a lost XON
Needs a timeout for robustnessnousually
Implementationcomparator + flip-flopdetector + 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

StimulusExpectedResult
resetnot pausedpass
ordinary byte 0x41no pause, not consumedpass
0x13 with flow control enabledpaused, consumedpass
ordinary byte while pausedstays pausedpass
0x11 with flow control enabledresumed, consumedpass
0x13 with flow control DISABLEDno pause, not consumed — it is payloadpass

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Where this fits

Part of the UART curriculum.