The Receiver: Oversampling and the Sample Point
Explain why a UART receiver samples sixteen times per bit, find where it takes each sample, and show what a short glitch or a sample at the edge of a bit does.
- Lesson
- P6.3
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A receiver clocked at the bit rate worked on the bench, where both ends shared one oscillator. In the product each end had its own crystal. Its one sample fell early or late in each bit, depending on when a frame arrived. Late in the bit, a small rate difference walked it into the next one.
- one sample per bit
- sixteen per bit (rx0)
Read the plot as a table
| Transmitter rate error | one sample per bit | sixteen per bit (rx0) |
|---|---|---|
| -3% | 46 | 64 |
| -2% | 55 | 64 |
| -1% | 60 | 64 |
| 0% | 64 | 64 |
| 1% | 56 | 64 |
| 2% | 55 | 64 |
| 3% | 54 | 64 |
Measured. The receiver with one sample per bit loses frames from the first step and keeps 46 of 64 at -3 %; the sixteen-tick receiver decodes all 64 at every step shown.
One Sample per Bit
A receiver with one clock per bit samples wherever its clock edge falls. That place, the phase, is set by when the start edge arrived: anywhere from 0 to 16 ticks into the bit. Late in the bit there is little margin, and a rate error eats it bit by bit.
| Rate error | One sample per bit | Sixteen per bit |
|---|---|---|
| -3 % | 46 of 64 | 64 of 64 |
| -2 % | 55 of 64 | 64 of 64 |
| -1 % | 60 of 64 | 64 of 64 |
| 0 % | 64 of 64 | 64 of 64 |
| 1 % | 56 of 64 | 64 of 64 |
| 2 % | 55 of 64 | 64 of 64 |
| 3 % | 54 of 64 | 64 of 64 |
Measured: the lesson's model rx_1x draws a phase per frame from a seeded generator; rx0 is the bench receiver on the same wire.
Sixteen Ticks per Bit
A clock sixteen times faster turns the phase into a count. The start edge is seen within one tick. Eight low ticks later is the middle of the start bit, and every sixteenth tick after that is the middle of the next bit.
- Find the start
The low-run counter resets on every high tick. Eight lows in a row mean a start bit, and that tick is the middle of it.
- Count to each middle
From the start, the bit counter fires every 16 ticks: the middle of data 0, data 1 and so on to the stop bit.
- Sample and deliver
Each sample is a vote of three ticks. After the stop sample the byte and its flags go to the holding register.
Read the diagram as text
- rx wire: no clock with it
- Low-run counter: counts consecutive low ticks
- Start check: 8 low ticks: a start
- Bit counter: every 16th tick: a sample
- Vote: 2 of the 3 ticks ending on the sample
- Shift register: data bits, then stop
- Holding register: data, valid, flags
- rx wire → Low-run counter
- Low-run counter → Start check: 8
- Start check → Bit counter: go
- Bit counter → Vote: sample now
- Vote → Shift register: bit
- Shift register → Holding register: after stop
Finding the start and finding the middle are one counter: eight ticks.
| Sample | Tick after the start edge | Reads |
|---|---|---|
| data 0 | 24 | 1 |
| data 1 | 40 | 0 |
| data 2 | 56 | 1 |
| data 3 | 72 | 0 |
| data 4 | 88 | 1 |
| data 5 | 104 | 0 |
| data 6 | 120 | 1 |
| data 7 | 136 | 0 |
| stop | 152 | 1 |
Measured for 0x55 with sample_log = 1. Tick n is the n-th receiver clock after the edge. Data 0 is sampled at tick 24, the stop bit at tick 152, sixteen apart.
The middle of data 0 is 24 ticks after the start edge, but the edge falls anywhere between two clock edges. So tick 24 lands up to one tick before the middle, never after it. The placement error is one sixteenth of a bit, and it does not depend on luck.
Eight Low Ticks
One low tick is not a start: noise does that. The receiver waits for eight in a row. Measured on an idle line: a 3-tick low prints nothing, a 7-tick low nothing, and an 8-tick low RX ff ok, a frame nobody sent.
Measured, one column per tick. The default receiver counts three lows and resets. The twin with qualify_1_tick = 1 starts a frame on the first low tick.
Bench defaults
glitch(3); #300;
send_byte(8'h55);
Measured: UART 55 ok, RX 55 ok.
rx0.qualify_1_tick = 1
rx0.qualify_1_tick = 1;
glitch(3); #300;
send_byte(8'h55);
Measured: RX ff ok, UART 55 ok, RX aa FE.
The twin delivers 0xFF with no flag at all, then misreads the real frame too. Starting on the first low tick also moves every sample seven ticks early, onto the edge of each bit. One counter does both jobs, so breaking one breaks the other.
module top(input clk,input rx);
reg [3:0] ticks=0;reg fire=0;reg [7:0] history=8'hff;reg was_full=0;
always @(posedge clk)begin
if(rx)ticks<=0;else if(ticks<8)ticks<=ticks+1;
fire<=!rx&&(ticks==7);
history<={history[6:0],rx};was_full<=history==0;
assert(fire==((history==0)&&!was_full));
end
endmoduleWhy the Middle
Bench defaults: 8, then 16
sample_log = 1;
send_byte(8'h55);
Samples at ticks 24, 40, 56, 72, 88, 104, 120, 136, 152. RX 55 ok.
rx0.sample_at_edge = 1: 8, then 8
rx0.sample_at_edge = 1;
sample_log = 1;
send_byte(8'h55);
Samples at ticks 16, 32, 48, 64, 80, 96, 112, 128, 144. RX aa FE.
Sampling at the edge of each bit fails even with matched clocks. The vote reads the three ticks ending on the sample, two of them in the bit before, so every bit shifts by one. A sample in the middle has half a bit of room on each side; lesson P6.4 measures how a rate error uses it.
Where You Will Meet It
Every UART receiver block, every bench monitor for one, and every bit-level problem in this module uses this counter. A design review asks where the sample sits; a bench proves it with a glitch on idle and a sweep of rate errors.
Asked in interviews: oversampling
Why does a UART receiver oversample?
There is no clock on the wire. A fast local clock lets it find the start edge within one tick and count to the middle of every bit.
Why wait for eight low ticks?
A short glitch must not start a frame, and eight ticks is also the middle of the start bit, so one counter both confirms the start and centres the samples.
Why 16 and not 2?
The sample can be off by one tick of the fast clock: half a bit at 2 times, a sixteenth at 16 times. Beyond that, rate mismatch dominates.
Try It
sample_log = 1 prints every sample the receiver takes, in ticks after the start edge. Data 0 is at tick 24: eight to the middle of the start bit, then sixteen. The stop sample is at tick 152, and the RX line follows it.
Add set_error(0.03); before send_byte: the ticks do not move, because the receiver counts its own clock. The wire moves under them; lesson P6.4 measures how far it can go. Then try glitch(7); and glitch(8); before the frame.
tick 24: data 0 sample reads 1 tick 40: data 1 sample reads 0 tick 56: data 2 sample reads 1 tick 72: data 3 sample reads 0 tick 88: data 4 sample reads 1 tick 104: data 5 sample reads 0 tick 120: data 6 sample reads 1 tick 136: data 7 sample reads 0 UART 55 ok tick 152: stop sample reads 1 RX 55 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
- With one sample per bit the sample point depends on when the frame arrived, and a small rate error loses frames.
- Sixteen ticks per bit place every sample within one tick of the middle of its bit.
- Eight consecutive low ticks both confirm a start and land in the middle of the start bit: one counter, two jobs.
- A receiver that starts on one low tick takes glitches as frames and samples every bit at its edge.
- Sampling at the edge of a bit fails even with matched clocks when the vote looks back.
Practice
- Move the Sample into the Bit Fix · Hard