Skip to content
VLSI Mentor

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:

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

SignalDirectionPolarityMeaning
rts_n_oout of this deviceactive low0 = far end may send · 1 = far end should stop
cts_n_iin to this deviceactive low0 = 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.

A peer-to-peer UART connection with hardware flow control, showing four signals and their directions. The local device's receive FIFO occupancy drives its flow control logic, which produces an active-low request-to-send output. That output crosses the cable to the far end's clear-to-send input, where it gates the far end's transmitter launch. Symmetrically, the far end's own receive occupancy drives its request-to-send output, which crosses back to this device's clear-to-send input and gates this device's transmitter launch. The data lines cross in the same way. Each direction's flow control is an independent mechanism: the local backpressure output says nothing about whether the local transmitter may send, and the local permission input says nothing about how full the local receive buffer is.local RX FIFOoccupancyflow ctrlhysteresisrts_n_oOUT, active lowfar cts_ngates far end TXlocal TXModule 7launch gate+ synchronisercts_n_iIN, active lowfar rts_nfar RX occupancylevelallowcablecablepermissionlaunch ok12
Figure 1 — the four signals and which way each one points. In a peer-to-peer connection the flow-control pair crosses: this device's backpressure output drives the far end's permission input. Each side's receive FIFO occupancy drives its own output, and each side's transmitter launch is gated by its own input — the two halves are independent mechanisms that happen to share a cable.

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:

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

baudT_framerecognition 0 µs10 µs50 µs
115,20086.81 µs2 bytes3 bytes3 bytes
1,000,00010.00 µs2 bytes3 bytes7 bytes
3,000,0003.33 µs2 bytes5 bytes17 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
H  =  DEPTH − STOP_LEVEL          must satisfy    H  ≥  in_flight  +  margin

For a 16-entry FIFO:

STOP_LEVELheadroom Hcovers 2 bytes?3 bytes?4 bytes?
88yesyesyes
106yesyesyes
124yesyesyes
133yesyesNO
142yesNONO
151NONONO

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stop   when level >= STOP_LEVEL
resume when level <= RESUME_LEVEL          with RESUME_LEVEL < STOP_LEVEL
between: HOLD

The 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

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

7. A Frame in Progress Is Never Truncated

The transmit gate contains one deliberate omission, and it is the chapter's second-most-important point:

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

A sequence showing the latency of hardware flow control. The receive FIFO's occupancy rises past the configured stop level, and the flow control logic deasserts permission by driving its request-to-send output to the inactive state. That signal propagates to the far end's clear-to-send input, where it must first pass through a synchroniser and then be recognised by the far end's transmit logic. Meanwhile the far end is already transmitting a character, which it completes, and it may already have committed a further character to its shift register, which it also sends. Only after those complete does it stop launching new frames. Every character emitted between the local threshold crossing and that point is already in flight and arrives regardless, which is why the threshold must leave headroom for them rather than being placed at the full boundary.RX FIFOFlow ctrlRTS/CTSFar end TXlevel >= STOP_LEVELrts_n_o driven high— 1-2 clockscts_n_i: synchronise+ recogniseFRAME IN PROGRESScompletes — 1 byteCOMMITTED next framesent — 1 bytenow stops launchingdrains toRESUME_LEVELrts_n_o low — farend may resume
Figure 2 — the full round trip. Time runs downward. Every stage between the occupancy crossing the stop level and the far end actually ceasing transmission contributes characters that are already committed, and the three marked in bold cannot be removed by any implementation choice on this side.

Stop at 12, resume at 6

13 cycles
A trace of thirteen character events showing occupancy in a sixteen-entry receive FIFO against a hysteresis pair. Occupancy rises to twelve, at which point permission is withdrawn and the request-to-send output goes inactive. Two further characters arrive because they were already in flight, taking occupancy to fourteen. The consumer then drains the queue: occupancy falls through eleven, ten, nine, eight and seven with permission still withheld, because the decision is held between the two thresholds, and is only restored when occupancy reaches six. Between those values nothing on the pin changes, so occupancy hovering near either threshold produces no chatter.held — no chatter between thresholdsheld — no chatter between thresholdsSTOP_LEVEL — permission withdrawnSTOP_LEVEL — permissionwithdrawn2 in-flight bytes still arrive2 in-flight bytes stillarriveRESUME_LEVEL — permission restoredRESUME_LEVEL — permissionrestoredlevel10111213141311987654allow_remote_orts_n_ot0t1t2t3t4t5t6t7t8t9t10t11t12
Figure 3 — hysteresis against occupancy. Columns are CHARACTER EVENTS — arrivals and consumer reads — not clock cycles. Permission is withdrawn when occupancy reaches 12 and is not restored until it falls to 6; between those two values the decision is held, so occupancy hovering near either threshold produces no change on the pin.

8. Verification

The suite tests the decision logic and the gate, with a positive control on both sides of every threshold.

StimulusExpectedResult
reset, low occupancypermission granted, rts_n_o = 0pass
level 11 (one below stop)still permittedpass
level 12 (== STOP_LEVEL)stopped, rts_n_o = 1pass
level 11, then 7 (between thresholds)still stopped — heldpass
level 6 (== RESUME_LEVEL)permitted againpass
oscillate 11↔12 twenty timeszero toggles on the pinpass
genuine drain to 6exactly one togglepass
cts_n_i denied, data waitingno launchpass
cts_n_i permitted, data waitinglaunch allowedpass
cts_n_i permitted, FIFO emptyno launchpass
cts_n_i withdrawn while tx_busy highgate blocks next launch onlypass

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.

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

9. Debugging

10. 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.

11. Understanding Check

12. 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.

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

13. 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

Where this fits

Part of the UART curriculum.