UART · Module 5
Start-Edge Detection and Start-Bit Validation
Detecting a departure from idle is a comparison on the synchronised input. Deciding it was a start bit rather than a disturbance is a judgement with a cost — and the receiver's entire frame timing hangs on which event it treats as the origin.
Chapter 5.1 enumerated four decisions a receiver must make. This chapter takes the first two, because in practice they are inseparable.
Detection is easy. On a synchronised input, a departure from idle is a comparison between this cycle's value and last cycle's — four lines of RTL, no judgement involved.
The second decision is where the engineering is:
A falling edge is evidence that a frame may have started. It is not proof.
A disturbance on the conductor produces an identical observation. A receiver that commits on every edge will construct frames nobody sent, and — worse than delivering garbage — it will have adopted a timing origin derived from a meaningless instant, which corrupts every position in the frame it thinks it is receiving.
So real receivers wait and check. That costs latency, spends timing budget, and raises a question that catches designs out: once you have waited, which event is the frame's phase reference — the edge you detected, or the moment you decided to believe it? Getting that wrong shifts every sample by a fixed amount, and the failure looks like a baud-rate error.
1. Detecting the Departure
On the synchronised signal, detection is a one-cycle history and a comparison.
// Synthesizable SystemVerilog — edge detection on the SYNCHRONISED input.
// rx_sync_q comes from the two-stage boundary of Chapter 5.1 §3. Detecting
// on the raw pin is the error that chapter rejected; everything here
// derives from one sampled version of the input.
logic rx_sync_d_q;
always_ff @(posedge clk or negedge rst_n) begin
// Idle level at reset, for the reason Chapter 5.1 gave: a receiver
// released from reset must not appear to have just seen a departure.
if (!rst_n) rx_sync_d_q <= 1'b1;
else rx_sync_d_q <= rx_sync_q;
end
// High for exactly one clk cycle, on the cycle the synchronised input is
// first observed at the space level having been at mark.
assign start_candidate = rx_sync_d_q && !rx_sync_q;start_candidate, not start_detected. The name is the chapter. This signal means a departure from idle was observed and nothing more.
It is one cycle wide by construction: the next cycle rx_sync_d_q has caught up and the expression is false. A pulse rather than a level is what downstream logic wants, because the event is instantaneous and the state it triggers is separate.
The observation is quantised to the clock, and there is fixed latency in front of it.
Physical edge to candidate pulse — fabric-clock scale
10 cyclesTwo quantities come out of this figure, and Chapter 4.5 already budgeted both.
A fixed delay, from the synchroniser stages. It is the same on every frame, so it shifts the receiver's whole view of the frame by a constant — which matters only if the design forgets to account for it.
A variable part, because the transition fell somewhere inside a clock period and the receiver cannot tell where. That is δ_origin, bounded by one clock period, and it is the irreducible part.
2. Why the Edge Is Not Enough
A departure from idle is produced by a start bit. It is also produced by:
- a disturbance coupled onto the conductor from something switching nearby;
- ringing or reflection on a long or unterminated line;
- a far end being powered on, reset, or hot-plugged mid-way;
- a floating input drifting across the receiver's threshold (Chapter 5.1 §6);
- the receiver being released from reset while a frame is already in progress, so a data transition is the first thing it sees.
The receiver cannot distinguish these from a real start at the moment of the edge, because at that moment the evidence is identical in every case.
3. Validation: Check That It Stayed
The standard answer is to wait and look again. If the departure was a real start bit, the line will still be at the space level partway through the interval; if it was a brief disturbance, it will not.
That requires a way to measure "partway through the interval", which is the sub-bit timing resolution Chapter 5.3 builds. This chapter takes it as given: a periodic oversample tick at M ticks per bit interval, produced as a clock enable in the clk domain. The natural validation point is the middle of the start interval, at M/2 ticks after the candidate — the same position a data sample would use, and the furthest point from both boundaries (Chapter 2.5).
False start — the line did not stay
10 cyclesThe counter convention, derived rather than asserted
This is where off-by-one errors live, so the convention must be explicit. Take the candidate cycle as phase 0 and advance on each subsequent oversample tick:
| Oversample tick | os_phase_q | rx_sync_q | Receiver interpretation |
|---|---|---|---|
| candidate | 0 | 0 | departure observed; counter loaded |
| +1 | 1 | 0 | qualifying |
| +2 | 2 | 0 | qualifying |
| … | … | … | … |
| +7 | 7 | 0 | qualifying |
| +8 | 8 | 0 | validation point — M/2 at M=16: still space, accept |
Eight tick intervals have elapsed between the candidate and the validation point, and the counter reads 8 because it was loaded with 0 at the candidate and incremented on each of the eight ticks since. Comparing against M/2 is therefore correct for this convention — and would be wrong by one interval if the counter were loaded with 1, or if the comparison were made before the increment.
// Synthesizable SystemVerilog — start qualification only.
// The oversample tick comes from Chapter 5.3; the frame reception that
// follows acceptance is Module 6. Deliberately no data, parity, stop or
// shift-register logic here.
module uart_start_qualify #(
parameter int unsigned OVERSAMPLE = 16
) (
input logic clk,
input logic rst_n,
input logic rx_sync_i, // synchronised input (Chapter 5.1)
input logic os_tick_i, // one pulse per oversample interval
input logic start_cand_i, // one-cycle candidate pulse (§1)
output logic start_accept_o, // one cycle: frame timing begins NOW
output logic qualifying_o
);
// Validation at the middle of the start interval. Written as a named
// constant derived from the parameter, never as a literal 8 — §5 of
// Chapter 5.3 shows what happens when OVERSAMPLE later changes.
localparam int unsigned VALIDATE_AT = OVERSAMPLE / 2;
// Counter spans 0..VALIDATE_AT, so its largest value is VALIDATE_AT.
localparam int unsigned PH_W =
(VALIDATE_AT <= 1) ? 1 : $clog2(VALIDATE_AT + 1);
initial begin
if (OVERSAMPLE < 2)
$fatal(1, "uart_start_qualify: OVERSAMPLE = %0d leaves no room to validate", OVERSAMPLE);
if (OVERSAMPLE % 2 != 0)
$warning("uart_start_qualify: OVERSAMPLE = %0d is odd; VALIDATE_AT = %0d truncates, placing the check %s of centre",
OVERSAMPLE, VALIDATE_AT, "just short");
end
logic [PH_W-1:0] os_phase_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
qualifying_o <= 1'b0;
os_phase_q <= '0;
start_accept_o <= 1'b0;
end else begin
start_accept_o <= 1'b0; // default: one-cycle pulse
if (!qualifying_o) begin
if (start_cand_i) begin
qualifying_o <= 1'b1;
os_phase_q <= '0; // phase 0 AT the candidate
end
end else if (os_tick_i) begin
if (os_phase_q == PH_W'(VALIDATE_AT - 1)) begin
// This tick makes the count VALIDATE_AT — the moment
// the table above calls the validation point.
qualifying_o <= 1'b0;
if (!rx_sync_i) start_accept_o <= 1'b1; // still space
// else: silently abandon, back to watching
end else begin
os_phase_q <= os_phase_q + 1'b1;
end
end
end
end
endmoduleWhy the comparison is against VALIDATE_AT - 1. The decision is taken on the tick that would carry the count to VALIDATE_AT, not on the following cycle. Comparing against VALIDATE_AT and acting the next tick validates one interval late — a 1/M UI shift of the whole frame, which at M = 16 is 6.25% of a bit and is exactly the kind of error that looks like a rate problem.
Reset and the default assignment. start_accept_o is assigned low at the top of the clocked block and overridden only on the accepting condition, which guarantees a single-cycle pulse without a separate clear. qualifying_o resets inactive so a receiver leaving reset is watching, not mid-qualification.
Odd OVERSAMPLE is warned, not rejected. OVERSAMPLE / 2 truncates, so an odd factor places the check slightly before the true centre. That is a legitimate design point — it still validates — but it is a different validation position than the parameter name suggests, and silently accepting it is how a design acquires an unexplained half-tick offset.
4. The Trade: Latency Against Confidence
Where the validation point sits is a real decision, and both directions cost something.
Validate early — say at M/4 rather than M/2. The receiver commits sooner and is ready for the rest of the frame with more margin in hand. But it only rejects disturbances shorter than a quarter of a bit; anything longer is accepted.
Validate late — at 3M/4, or by requiring the line to remain at space across several checks. Stronger rejection, and every tick spent qualifying is a tick during which the receiver is committed to nothing and the frame is advancing. Push it far enough and the qualification runs past the start interval entirely.
Validate at M/2 is the common choice because it is simultaneously the most informative single point — furthest from both boundaries, so least sensitive to the timing error of Chapter 4.5 — and exactly where a data sample would fall, which means the same mechanism serves both.
5. Verification
Qualification is a mechanism whose whole purpose is behaviour under inputs that a well-formed-frame testbench never produces.
Glitch width is the primary axis, and the interesting values bracket the validation point:
| Disturbance width | Expected behaviour |
|---|---|
| shorter than the synchroniser can register | not observed at all — no candidate |
observed, but gone well before M/2 | candidate raised, rejected at validation |
| ends just before the validation tick | rejected — the boundary case |
| ends just after the validation tick | accepted — a fabricated frame follows |
| longer than a full bit interval | accepted; indistinguishable from a real start at this stage |
The fourth row is not a defect. It is the mechanism's specified limit, and a testbench should assert it as expected behaviour rather than treating it as a failure. A validation point rejects disturbances shorter than itself and nothing more, and pretending otherwise leads to a design review conversation in which someone promises glitch immunity the architecture does not provide.
Glitch phase is a second axis. A disturbance of fixed width arriving at different offsets relative to the oversample tick grid is observed at different ticks, so it can land either side of the boundary. Sweeping width alone, at a fixed phase, finds one boundary; sweeping both finds the region.
Two further scenarios belong here and are easy to omit:
- Reset released mid-frame. The receiver sees a data transition as its first departure from idle, qualifies it successfully — the line does stay at space for that bit — and receives a fabricated frame. This is correct behaviour and the recovery path is worth establishing: the design should return to a sane state within a frame or two, and a test should confirm which.
- A disturbance during qualification of a real start. The line is at space and a brief excursion to mark occurs before the validation tick. Whether the design cares depends on whether it checks continuously or only at the validation point — the module in §3 checks only at the point, so it accepts. That is a defensible choice, and it is one a testbench should pin down rather than discover.
// Testbench SystemVerilog — NOT synthesizable. Drives a disturbance of a
// given width at a given phase, then checks the receiver's verdict.
task automatic inject_glitch(input realtime width, input realtime phase);
#(phase);
rx_i <= 1'b0;
#(width);
rx_i <= 1'b1;
endtask
// Sweep both axes around the validation point. T_BIT and OVERSAMPLE come
// from the same parameters the DUT was built with, so the expected
// boundary moves with the configuration rather than being hard-coded.
initial begin
realtime validate_at = T_BIT * VALIDATE_AT / OVERSAMPLE;
foreach (phase_list[p])
for (realtime w = validate_at*0.8; w <= validate_at*1.2; w += T_BIT/64) begin
inject_glitch(w, phase_list[p]);
// Expectation is derived, not tabulated: a disturbance that
// has ended before the validation instant must be rejected.
expect_accept = (w > validate_at);
@(negedge qualifying);
assert (start_accept === expect_accept)
else $error("width %0t phase %0t: accept=%b expected %b", w, phase_list[p], start_accept, expect_accept);
end
endThe expectation is computed, not listed. A tabulated set of expected results is correct for one OVERSAMPLE and silently wrong after it changes — which is Chapter 5.3's recurring hazard.
Digital injection is not a noise model. This verifies the receiver's response to defined input disturbances. Real analogue behaviour — a slow edge crossing the threshold repeatedly, ringing, a level that hovers near the threshold — is a board-level concern that a digital testbench cannot represent and a digital receiver largely cannot fix.
An assertion worth having, because it states the contract rather than the implementation:
// Assertion — acceptance never occurs without qualification having run.
property p_no_unqualified_accept;
@(posedge clk) disable iff (!rst_n)
start_accept_o |-> $past(qualifying_o);
endproperty
assert property (p_no_unqualified_accept);6. What This Means on an FPGA
The candidate is late and you cannot make it early. Two synchroniser stages plus the edge comparison put the candidate two to three clock periods after the physical transition. That delay is fixed and budgeted; what cannot be recovered is where inside a clock period the transition fell. Both were priced in Chapter 4.5.
Validation is the cheapest noise rejection available to the design, and the only one that costs no extra hardware — the counter and the tick are needed anyway. Input filtering, if a board needs it, is a board-level or I/O-level decision that belongs below this logic.
A floating rx_i defeats qualification completely. A drifting input can sit at the space level for far longer than the validation point, so the receiver accepts, fabricates a frame, and repeats indefinitely. Qualification rejects brief disturbances; it has no defence against an input with no defined level, which is a board fix (Chapter 5.1 §6).
7. Understanding Check
8. Summary
Detection is a comparison on the synchronised input and produces a one-cycle candidate pulse. The physical transition and that pulse are separated by fixed synchroniser latency plus an irreducible uncertainty about where inside a clock period the transition fell — the fixed part shifts everything equally, the variable part is Chapter 4.5's δ_origin.
A falling edge is evidence, not proof. Disturbances, ringing, power-up, hot-plug, a floating input and a mid-frame reset release all produce the identical observation. Accepting one costs a fabricated frame, a lost real frame, and a misleading framing error — because acceptance sets the timing origin for everything that follows.
Validation waits to the middle of the start interval and checks the line is still at space. M/2 is the common choice because it is the point least sensitive to timing error and is where a data sample would fall anyway. Earlier validation commits sooner and rejects less; later validation rejects more and spends budget, bounded above by the start interval itself. The mechanism rejects disturbances shorter than the validation delay, and nothing more.
The counter convention must be derived, not asserted: with phase 0 loaded at the candidate, eight tick intervals have elapsed when the counter reads 8, so the decision is taken on the tick carrying the count to M/2. Acting one tick later shifts the whole frame by 1/M of a bit.
And the sharpest correctness question is which event is the origin — the candidate or the acceptance. Both are usable; mixing them places every sample half a bit late, producing corruption that looks like a rate error on a link whose rate is correct.
9. What Comes Next
This chapter has used an oversample tick throughout without saying where it comes from, how many there are per bit interval, or what that choice costs. Chapter 5.3 builds it: what oversampling actually means, why 8× and 16× are the familiar factors and why neither is required by the framing, what a higher factor buys in placement resolution, and what it costs in tick generation — including the fact that the rate a 16× receiver needs turns out to be exactly the frequency Chapter 4.2 said the historic crystals were chosen to produce.
Browse the full path on the UART tutorials index. For the timing origin treated as a protocol object rather than an implementation choice, see Chapter 2.3.
Continue learning
Related tutorials
- Related topic
Start-Bit Synchronisation and Per-Frame Timing Recovery
The receiver knows the rate but not the phase. One guaranteed transition per frame supplies the missing half, and the reconstruction is discarded and rebuilt at the next frame rather than held across the stream — which is why nothing about the two clocks is ever synchronised.
- Related topic
Idle, Start Bit and Frame Entry
The start bit does two separate jobs: its leading transition fixes the frame's timing origin, and the interval that follows occupies a full bit cell at space. Conflating an instant with a duration is the source of most confusion about where a UART frame begins.
- Related topic
Data Bits and LSB-First Transmission
Three different orderings get called bit order: numerical significance, array index, and time on the wire. UART fixes only the third, and the byte is never reversed — a verified byte-to-wire walkthrough, the shift-register invariant, and how to annotate a capture without fooling yourself.
- Related topic
Parity Generation, Checking and Error Detection
One interval, one XOR reduction, and a detection guarantee with a sharp edge: parity catches every corruption that flips an odd number of protected bits and provably misses every even-numbered one — demonstrated, not asserted.
Where this fits
Part of the UART curriculum.
