Flow Control, FIFOs and Baud Divisors
Explain what RTS and CTS buy over XON/XOFF, place a FIFO trigger, and compute a baud divisor and its error against the measured budget.
- Lesson
- P6.6
- Time
- 30 min
- Runs on
- Your browser
- Check
- 4 questions
A console streamed logs at 115200 into a receiver whose software read one byte per interrupt. Under load the interrupt came late, the 16-byte FIFO filled, and the hardware dropped bytes behind a flag nobody polled. The reference design routed RTS and CTS to the connector and left them unconnected, because "the link is only 115200".
Read the diagram as text
- Sending software: writes 24 bytes
- txq: 16 deep; offers a byte while CTS is low
- Transmitter: one frame at a time
- Receiver: one byte per stop sample
- rxq: 16 deep; RTS high from 12
- Slow reader: reads nothing here
- Sending software → txq: push
- txq → Transmitter: send
- Transmitter → Receiver: TX → RX
- Receiver → rxq: push
- rxq → txq: RTS → CTS
- rxq → Slow reader: pop
RTS means "I can take more"; it comes from the receiving FIFO's fill level. The sender checks CTS before each frame.
RTS and CTS
Both are "ready" signals, active low. The receiving FIFO raises rts_n once its count reaches the trigger. That wire is the sender's cts_n, and the sender starts no new frame while it is high.
Measured, one column per frame time (176 ticks). The count climbs to the trigger, rts_n rises, the sender stops. End: sent 12, rxq holds 12, txq holds 12, lost 0.
Measured. rts_n rises at the same point and nothing listens. The FIFO fills to 16, overrun rises on the next push. End: sent 24, rxq holds 16, lost 8.
| Trigger | Frames sent | rxq holds | Lost | Overrun |
|---|---|---|---|---|
| 12 (default) | 12 | 12 | 0 | 0 |
12, trigger_off_by_one | 13 | 13 | 0 | 0 |
| 16 | 16 | 16 | 0 | 0 |
16, trigger_off_by_one | 24 | 16 | 8 | 1 |
Measured. This sender checks CTS before every frame and the receiver counts a byte at its stop sample, so nothing is in flight: the count stops exactly at the trigger. A trigger that can never be reached loses bytes.
A real transmitter often loads its next byte into a shift register before the current frame ends. That byte is in flight when RTS rises, and so is anything in a cable or a bridge. The room above the trigger holds them.
XON and XOFF
Software flow control sends "stop" and "go" as characters in the data stream: XOFF is 0x13, XON is 0x11. It costs no wires.
Read phases and sample points (transmitter half bit)
- start: columns 2 through 3.
- data 0: columns 4 through 5.
- data 1: columns 6 through 7.
- data 2: columns 8 through 9.
- data 3: columns 10 through 11.
- data 4: columns 12 through 13.
- data 5: columns 14 through 15.
- data 6: columns 16 through 17.
- data 7: columns 18 through 19.
- stop: columns 20 through 21.
- rx sampled at 4.688, 4.813, 4.938, 6.688, 6.813, 6.938, 8.688, 8.813, 8.938, 10.688, 10.813, 10.938, 12.688, 12.813, 12.938, 14.688, 14.813, 14.938, 16.688, 16.813, 16.938, 18.688, 18.813, 18.938, 20.688, 20.813, 20.938.
Generated and decoded back. To the wire it is just a byte.
Read phases and sample points (transmitter half bit)
- start: columns 2 through 3.
- data 0: columns 4 through 5.
- data 1: columns 6 through 7.
- data 2: columns 8 through 9.
- data 3: columns 10 through 11.
- data 4: columns 12 through 13.
- data 5: columns 14 through 15.
- data 6: columns 16 through 17.
- data 7: columns 18 through 19.
- stop: columns 20 through 21.
- rx sampled at 4.688, 4.813, 4.938, 6.688, 6.813, 6.938, 8.688, 8.813, 8.938, 10.688, 10.813, 10.938, 12.688, 12.813, 12.938, 14.688, 14.813, 14.938, 16.688, 16.813, 16.938, 18.688, 18.813, 18.938, 20.688, 20.813, 20.938.
Generated and decoded back.
Because the signals are bytes, binary data that contains 0x11 or 0x13 must be escaped. And the stop must be received, read by software and acted on, so it arrives late by the receiver's FIFO depth and the reader's delay. RTS and CTS act in hardware, before the next frame.
Baud Divisors
The receiver's tick is the system clock divided by an integer: divisor = round(clock / (16 × baud)). So the baud is quantised, and the error adds to the peer's.
| Baud | 1.8432 MHz | 50 MHz | 100 MHz |
|---|---|---|---|
| 9600 | 12: 0 % | 326: -0.15 % | 651: +0.01 % |
| 19200 | 6: 0 % | 163: -0.15 % | 326: -0.15 % |
| 38400 | 3: 0 % | 81: +0.47 % | 163: -0.15 % |
| 57600 | 2: 0 % | 54: +0.47 % | 109: -0.45 % |
| 115200 | 1: 0 % | 27: +0.47 % | 54: +0.47 % |
| 230400 | 1: -50 % | 14: -3.12 % | 27: +0.47 % |
| 460800 | 1: -75 % | 7: -3.12 % | 14: -3.12 % |
| 921600 | 1: -87.5 % | 3: +13.03 % | 7: -3.12 % |
Computed: divisor, then the error of the actual rate. A red cell alone would fail the measured 8N1 back-to-back sweep against an exact receiver. That sweep first fails at −6 % and +4.5 % (lesson P6.4); amber cells use more than half of it.
A 1.8432 MHz clock divides exactly to every classic rate, which is why it was a common UART crystal. A 50 MHz clock gives small errors at low rates and large ones near the top, where the divisor is small. Remember the peer: its error adds to yours.
Where You Will Meet It
Every UART block has a divisor register and a FIFO with a trigger; many have automatic RTS and CTS. A bench drives the reader slow on purpose, checks that overrun never rises with flow control on, and checks the divisor against the budget for every clock the chip supports.
Asked in interviews: flow control
What do RTS and CTS mean?
Both are "ready to receive" signals. A side drives RTS from its own FIFO level; the other side sees it as CTS and starts no new frame while it is deasserted.
Why is the RTS trigger below the FIFO size?
Bytes already started or loaded still arrive after RTS changes; the room above the trigger holds them.
Why can XON/XOFF not carry binary data as is?
The stop and go signals are the bytes 0x13 and 0x11, so binary data containing them must be escaped.
Try It
The producer writes 24 bytes into txq, the sender sends them while CTS allows, and nothing reads rxq. With flow control honoured, 12 frames go out, rxq holds 12, txq still holds 12, nothing is lost.
Add txq.cts_ignored = 1; to the setup, then try rxq.trigger = 16; with and without rxq.trigger_off_by_one = 1;.
sent 12, rxq holds 12, txq holds 12, lost 0, overrun 0Read included file: uart_tx.sv
`timescale 1ns/1ps
module uart_tx(output reg tx=1);
real error=0.0; int bits=8, parity=0; real stop_bits=1.0;
bit lsb_first=1, parity_includes_start=0, parity_includes_stop=0, parity_wrong_sense=0, short_stop=0, no_idle_between=0;
task automatic bit_time(input bit v); tx=v; #(16.0*(1.0+error)); endtask
task automatic send_byte(input byte unsigned value);
bit p; p=(parity==2);
bit_time(0);
for(int b=0;b<bits;b++) begin bit_time(value[lsb_first?b:bits-1-b]); p=p^value[b]; end
if(parity!=0) bit_time(p ^ parity_includes_stop ^ parity_wrong_sense);
tx=1; #(16.0*(1.0+error)*(short_stop?0.25:stop_bits));
// no_idle_between is a legal back-to-back frame control, not a fault.
if(!no_idle_between) #(16.0*(1.0+error));
endtask
task automatic send_break(); tx=0; #(200*16); tx=1; #32; endtask
task automatic set_error(input real e); error=e; endtask
endmodule
Read included file: uart_rx.sv
`timescale 1ns/1ps
module uart_rx(input wire clk, rst_n, rx, take,
output reg [7:0] data=0, output reg valid=0, frame_err=0, parity_err=0, overrun=0, break_seen=0, frame_done=0);
int bits=8, parity=0; real stop_bits=1.0;
bit trace=0;
bit parity_includes_start=0; // XOR of the zero-valued START is an equivalent control.
bit sample_at_edge=0, qualify_1_tick=0, no_vote=0, no_resync=0, rearm_on_low=0, stop_as_data=0, no_overrun_flag=0, parity_includes_stop=0;
int state=0, low_run=0, left_ticks=0, index=0, votes=0, frames=0, ticks=0, next_data_tick=0;
bit armed=1, sampled=0, p=0, pe=0, pending_zero_fe=0; reg [7:0] shift=0;
always @(posedge clk) begin
frame_done=0;
if(!rst_n) begin state=0;low_run=0;left_ticks=0;index=0;votes=0;valid=0;data=0;frame_err=0;parity_err=0;overrun=0;break_seen=0;armed=1;frames=0;ticks=0;next_data_tick=0;pending_zero_fe=0; end
else begin
ticks++;
if(take)valid=0;
low_run=rx?0:low_run+1;
if(rx)break_seen=0;
if(low_run>=176&&!rearm_on_low)begin break_seen=1;state=4;pending_zero_fe=0;end
if(state==4)begin if(rx)begin
if(pending_zero_fe)begin
if(valid&&!no_overrun_flag)overrun=1;
data=0;valid=1;frame_err=1;parity_err=pe;frames++;frame_done=1;pending_zero_fe=0;
end
state=0;armed=1;
end end
else if(state==0)begin
if(rx)begin armed=1;end
else if(armed&&low_run>=(qualify_1_tick?1:8))begin
state=1;left_ticks=sample_at_edge?8:16;index=0;shift=0;p=(parity==2)^parity_includes_stop;votes=0;pe=0;
// Fault: later frames keep the first frame's free-running schedule.
// At nominal back-to-back pacing it can work; a new START must normally
// reset this timing so baud error cannot accumulate across frames.
if(no_resync&&frames>0)begin
while(next_data_tick<=ticks)next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
left_ticks=next_data_tick-ticks;
end else next_data_tick=ticks+left_ticks;
next_data_tick+=16*(1+bits+(parity!=0))+$rtoi(16*stop_bits);
end
end else begin
if(left_ticks<=3)votes=votes+(rx?1:0);
left_ticks=left_ticks-1;
if(left_ticks==0)begin
sampled=no_vote?rx:(votes>=2);votes=0;left_ticks=16;
if(trace)$display("UART SAMPLE t=%0t state=%0d bit=%0d value=%0d",$time,state,index,sampled);
if(state==1)begin shift[index]=sampled;p=p^sampled;index=index+1;if(index==bits)state=parity==0?3:2;end
else if(state==2)begin pe=(sampled!=p);state=3;end
else begin
if(shift==0&&!sampled&&!rearm_on_low)pending_zero_fe=1;
else begin
if(valid&&!no_overrun_flag)overrun=1;
data=stop_as_data?{sampled,shift[7:1]}:shift;
frame_err=!sampled;parity_err=pe;valid=1;frames=frames+1;frame_done=1;
end
state=(sampled||rearm_on_low)?0:4;armed=1;
end
end
end
end
end
endmodule
Read included file: uart_flow.sv
`timescale 1ns/1ps
module uart_flow(input wire clk,rst_n,push,pop,cts_n,input wire [7:0] din,
output wire rts_n,output wire send,output wire [7:0] dout,output reg overrun=0);
reg [7:0] fifo[0:15]; int head=0,count=0,trigger=12;
bit cts_ignored=0,trigger_off_by_one=0;
assign rts_n=count>=(trigger+(trigger_off_by_one?1:0));
assign send=count>0&&(!cts_n||cts_ignored);
assign dout=count>0?fifo[head]:8'h00;
always @(posedge clk)begin
if(!rst_n)begin head=0;count=0;overrun=0;end
else begin
if(pop&&send)begin head=(head+1)%16;count=count-1;end
if(push)begin if(count==16)overrun=1;else begin fifo[(head+count)%16]=din;count=count+1;end end
end
end
endmodule
Check Yourself
Remember
- RTS and CTS are "ready" signals: the receiving FIFO drives RTS, the sender sees it as CTS and starts no new frame.
- Ignoring CTS fills the FIFO and every byte after it is lost behind an overrun flag.
- The trigger sits below full so bytes already in flight have room.
- XON/XOFF needs no wires but uses two byte values and acts late, through software.
- The divisor quantises the baud; its error adds to the peer's and must fit the measured budget.
Practice
- RTS and CTS Around a Byte FIFO Implement · Medium
- A Compact UART Voter and Sampler Hard