Serial Links in One Picture
Sort the five off-chip serial links by two questions, read one byte on four of them, and say what each answer costs the receiver.
- Lesson
- P6.1
- Time
- 20 min
- Runs on
- Your browser
- Check
- 4 questions
A board passed bring-up on the bench, then lost bytes from a sensor in the field. The sensor spoke UART, and the firmware had set its baud divisor "close enough" without measuring how close. Nobody could say what the link tolerated, because nobody had drawn a frame against the receiver's sample points.
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. A start bit, eight data bits least significant first, a stop bit: 10 bit-times for one byte. The dots are where a receiver samples. Module P6 is this picture with its tolerance measured.
Two Questions Sort the Links
Five links leave a typical SoC. Ask each one two questions. Does a clock wire travel with the data? How is a 1 driven: pushed high, or released to a pull-up? The first answer sets the receiver's job, the second sets speed and sharing.
- No clock wire
Only UART sends no clock. Its receiver must find every bit from one falling edge per frame (lesson P6.3).
- A clock, pushed
SPI and QSPI send a clock and drive both levels. Fast, but one driver per wire at a time (module P7).
- Released to a pull-up
I2C only ever pulls low; a resistor brings the line high, slowly. Any device can pull, so many share two wires (module P8). I3C mixes both styles (module P9).
Read the diagram as text
- SoC: five off-chip links
- Debug console: UART: TX and RX, no clock
- Boot flash: QSPI: clock, select, 4 data
- Sensor: SPI: clock, select, 2 data
- Power controller: I2C: SCL and SDA
- Sensor hub: I3C: SCL and SDA
- SoC → Debug console: no clock · push-pull
- SoC → Boot flash: clock · push-pull
- SoC → Sensor: clock · push-pull
- SoC → Power controller: clock · open drain
- SoC → Sensor hub: clock · both styles
No clock wire means the receiver recovers timing from the data. Open drain means the rise is slow but the wire can be shared.
One Byte, Four Shapes
The same byte, 0xA5, on four of the links. Each figure is generated and read back by its own decoder before the page is built. Count the bits and note who drives each lane.
Read phases and sample points (half clock)
- mode 0: columns 1 through 18.
- MOSI sampled at 3, 5, 7, 9, 11, 13, 15, 17.
- MISO sampled at 3, 5, 7, 9, 11, 13, 15, 17.
8 clocks, most significant bit first, framed by chip select going low. The controller drives CLK and MOSI; the peripheral drives MISO in the same clocks.
Read phases and sample points (half clock)
- command: columns 1 through 16.
- address: columns 17 through 28.
- mode: columns 29 through 32.
- dummy: columns 33 through 40.
- data: columns 41 through 44.
A quad read: command, address, mode and dummy clocks first, then the data at a nibble per clock on four pins. The byte itself takes 2 clocks. One command, four lanes, not four links.
Read phases and sample points (quarter bit)
- START: columns 2 through 3.
- 0xa0 + ACK: columns 4 through 39.
- 0xa5 + ACK: columns 40 through 75.
- STOP: columns 76 through 79.
- SDA sampled at 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 74.
After START and the address byte, the data byte takes 9 clocks: eight data bits, most significant first, and a ninth in which the receiver pulls SDA low to acknowledge. The two driver lanes show who pulls the line.
The UART frame in the opening figure has no clock lane at all. Its start bit is the only edge the receiver is promised, and its stop bit guarantees the next start is an edge too.
The Comparison
| Link | Wires | Clock from | A 1 is | Per payload byte | At a common rate |
|---|---|---|---|---|---|
| UART | TX, RX | nobody: each side has its own | driven high | 10 bit-times | 86.8 µs at 115200 baud |
| SPI | CLK, CS_n, MOSI, MISO | the controller | driven high | 8 clocks | 60.2 ns at 133 MHz |
| QSPI | CLK, CS_n, IO0-IO3 | the controller | driven high | 2 clocks (data phase) | 15.0 ns at 133 MHz |
| I2C | SCL, SDA | the controller | released to a pull-up | 9 clocks | 22.5 µs at 400 kHz |
| I3C | SCL, SDA | the controller | driven or released | module P9 | module P9 |
Counts from the four figures. The rates are the usual top rates: a common console baud, the flash's 133 MHz maximum clock, I2C Fast-mode. Command, address and acknowledge overheads differ per link.
The table also gives the module order. UART first, because it has no clock and every bit is found by counting. Then SPI and QSPI, where edges and phases matter. Then I2C, where two devices can drive one wire.
Read the diagram as text
- P6.2 The frame: start, data, parity, stop
- P6.3 The receiver: sixteen ticks per bit
- P6.4 Baud mismatch: the measured budget
- P6.5 Errors and break: which flag, which cause
- P6.6 Flow control: RTS, CTS, FIFOs, divisors
- P6.7 Capstone: monitor, checker, bug hunt
- RTL R8.2: a UART with FIFOs
- P6.2 The frame → P6.3 The receiver
- P6.3 The receiver → P6.4 Baud mismatch
- P6.4 Baud mismatch → P6.5 Errors and break
- P6.5 Errors and break → P6.6 Flow control
- P6.6 Flow control → P6.7 Capstone
- P6.7 Capstone → RTL R8.2: design side
Every page after this one runs the same bench: a transmitter, a sixteen-tick receiver and a wire monitor.
Where You Will Meet It
Every chip has a UART console, usually the first thing that talks at bring-up. A flash boots it over QSPI, sensors sit on SPI or I2C, and the power controller is on I2C. A verification engineer meets all four in one SoC bench.
Asked in interviews: the serial family
Which serial link has no clock wire, and what does that cost?
UART. The receiver must find the bit times from the start edge and its own clock, so both ends must agree on the rate closely.
Why is I2C slower than SPI?
A 1 on I2C is a released line pulled up by a resistor, so the rise is slow. SPI drives both levels and runs far faster.
Is QSPI four SPI links?
No. One controller sends one command and uses four data lanes together; a byte takes two clocks.
Try It
Two lines. UART a5 ok is the wire monitor: it watches the line and decodes what was sent. RX a5 ok is the receiver on the bench: what it delivered to its holding register. They agree, and neither saw an error.
Change the byte and run it again. Then add tx0.parity = 1; before send_byte: the monitor follows the transmitter, the receiver still expects no parity, and you will see what a mismatched setting looks like. Lesson P6.2 explains the result.
UART a5 ok RX a5 okRead 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
Read included file: uart_checker.sv
`timescale 1ns/1ps
module uart_checker(input wire clk,rx,valid,frame_err,parity_err,input wire [7:0] data,input wire frame_done);
int broken=0,received=0;
task automatic expect_byte(input byte unsigned wanted,input bit fe=0,pe=0);
if(!valid||data!==wanted||frame_err!==fe||parity_err!==pe)begin broken++;$display("UART CHECK got=%02h valid=%b FE=%b PE=%b expected=%02h FE=%b PE=%b",data,valid,frame_err,parity_err,wanted,fe,pe);end
endtask
always @(negedge clk)if(frame_done)received++;
endmodule
Read included file: uart_mon.sv
`timescale 1ns/1ps
// Independent wire decoder: completion is reported after the stop sample, never at START.
module uart_mon(input wire rx);
int bits=8,parity=0; real bit_period=16.0; int frames=0; bit trace=0;
reg [7:0] value; bit p,pe,fe;realtime began;
initial forever begin
@(negedge rx);began=$realtime; #(bit_period/2); if(rx==0)begin
value=0;p=(parity==2);pe=0;
for(int i=0;i<bits;i++)begin #(bit_period);value[i]=rx;p=p^rx;end
if(parity!=0)begin #(bit_period);pe=(rx!=p);end
#(bit_period);fe=!rx;
if(value==0&&fe)begin
while(rx==0&&$realtime-began<11*bit_period)#(bit_period/16);
if(rx==0)begin wait(rx==1);if(trace)$display("UART -- BREAK");end
else begin frames++;if(trace)$display("UART 00 FE");end
end
else begin frames++;if(trace)$display("UART %02h %s",value,fe?"FE":pe?"PE":"ok");end
end
end
endmodule
Read included file: uart_tasks.svh
task automatic send_byte(input byte unsigned b); tx0.send_byte(b); endtask
task automatic send_break(); tx0.send_break(); endtask
task automatic set_error(input real e); tx0.set_error(e); endtask
task automatic consume(); @(negedge clk);take=1;@(negedge clk);take=0;endtask
Read included file: p6_lab.svh
// p6_lab.svh: lesson helpers. The wire monitor follows the transmitter's format and rate, so its
// UART line shows what was sent; the RX line shows what the receiver under test delivered.
bit rx_log = 1, sample_log = 0;
int ticks = 0, edge_tick = 0, last_left = 0, last_state = 0, last_index = 0;
always @(posedge clk) ticks++;
always @(negedge rx) if (rx0.state == 0) edge_tick = ticks;
always @(tx0.parity) mon.parity = tx0.parity;
always @(tx0.bits) mon.bits = tx0.bits;
always @(tx0.error) mon.bit_period = 16.0 * (1.0 + tx0.error);
always @(negedge clk) begin
if (sample_log && last_left == 1 && rx0.left_ticks == 16) begin
if (last_state == 1) $display("tick %0d: data %0d sample reads %b", ticks - edge_tick, last_index, rx0.sampled);
else if (last_state == 2) $display("tick %0d: parity sample reads %b", ticks - edge_tick, rx0.sampled);
else $display("tick %0d: stop sample reads %b", ticks - edge_tick, rx0.sampled);
end
last_left = rx0.left_ticks; last_state = rx0.state; last_index = rx0.index;
end
// The RX line is printed one bit-time after the receiver's stop sample, after the monitor's line.
always @(rx0.frames) if (rx_log) begin
#16;
if (overrun) $display("RX %02h %s overrun", data, frame_err ? "FE" : parity_err ? "PE" : "ok");
else $display("RX %02h %s", data, frame_err ? "FE" : parity_err ? "PE" : "ok");
end
always @(posedge break_seen) if (rx_log) begin wait (rx == 1); #16 $display("RX break"); end
// A low pulse of the given number of ticks on the idle line.
task automatic glitch(input int width); tx0.tx = 0; #(width); tx0.tx = 1; endtask
// Send a byte and pull the line low for `width` ticks ending at receiver tick `at` after its start edge
// (tick n is the n-th clock edge after the edge), then give the line back its level.
int hit_at = 0, hit_width = 0; reg hit_level = 1; event hit_go;
always @(hit_go) begin
#(hit_at - 0.5 - hit_width + 0.25); hit_level = tx0.tx; tx0.tx = 0; #(hit_width); tx0.tx = hit_level;
end
task automatic send_hit(input byte unsigned b, input int at, input int width);
hit_at = at; hit_width = width; -> hit_go; send_byte(b);
endtask
Check Yourself
Remember
- Two questions sort the serial links: is there a clock wire, and is a 1 driven or released to a pull-up.
- UART alone has no clock wire, so its receiver finds every bit by counting from the start edge.
- SPI and QSPI drive both levels with a controller clock; I2C only pulls low and shares the line.
- One 8N1 UART byte is ten bit-times on the wire; SPI takes eight clocks, QSPI two, I2C nine.
Practice
- Encode a UART Frame Implement · Easy