UART · Module 9
Error Status, Sticky Flags and Receiver Re-Synchronisation
Transient events become persistent state, and the design decision is what happens when a clear and a new event land on the same clock edge. Detection and recovery are different problems.
Five chapters have produced events: a framing verdict, a parity verdict, an overrun pulse, a break declaration and a release. Each is correct, each is transient, and none of them is what software reads.
This chapter closes the module by answering the two questions the events leave open.
How do transient events become stable state? A one-cycle pulse on a 100 MHz clock is invisible to anything that samples less often than every 10 ns, which is everything. Something must hold it — and how it holds it, particularly when a clear collides with a new event, is a design decision with a defensible answer rather than an accident of statement ordering.
How does the receiver get back to a trustworthy frame boundary? This is a genuinely different problem from detection, and the distinction is the chapter's second half:
An implementation can detect every error correctly and still recover badly — turning one malformed frame into a burst of them.
1. Event and State
The distinction runs through every error source in this module, which is why they were all built to produce the same two shapes.
| Event | State | |
|---|---|---|
| Example | frame_error_evt_o | frame_error_sticky_o |
| Shape | one-cycle pulse | level, held |
| Answers | did something happen on this clock? | has something happened since I last looked? |
| Lifetime | one cycle | until cleared or reset |
| Consumer | hardware — counters, aggregators | software, or a slow observer |
| Loses information? | no | yes — how many, and which |
Software cannot observe events. A driver polling a status register every millisecond sees one sample in every hundred thousand clock cycles; a one-cycle pulse is invisible with certainty. Every event in this module therefore needs something that holds it, and that something is the sticky flag.
But the flag loses information the event had. A sticky bit that is set tells you at least one error occurred since the last clear — not how many, not when, not which frame. Chapter 9.3 §6 made the same point about overrun specifically, and it generalises: a counter is strictly more informative than a flag, for the cost of a few flip-flops, and a design that only ever needs "did anything go wrong" is choosing the weaker primitive deliberately rather than by default.
There is a third shape, and Chapter 9.4 produced the only instance of it: break_active_o is a level that is neither an event nor a sticky flag. It describes the line's present condition rather than a past occurrence, so it clears itself when the condition ends. A design that made break sticky and only sticky would be unable to answer "is the link broken right now?", which is usually the more urgent question.
2. The RTL
// ---------------------------------------------------------------------------
// 9.6 — sticky status, SET-DOMINANT, with an explicit clear input.
// ---------------------------------------------------------------------------
module uart_err_status (
input logic clk,
input logic rst_n,
input logic frame_error_evt_i,
input logic parity_error_evt_i,
input logic overrun_evt_i,
input logic break_evt_i,
input logic clear_i, // software clear, one cycle
output logic frame_error_sticky_o,
output logic parity_error_sticky_o,
output logic overrun_sticky_o,
output logic break_sticky_o,
output logic any_error_o
);
// SET-DOMINANT: when a clear and a new event land on the same cycle the
// flag STAYS SET. Losing a real event to a clear that raced it is the
// worse failure for diagnosis — §5 of the chapter argues this.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
frame_error_sticky_o <= 1'b0;
parity_error_sticky_o <= 1'b0;
overrun_sticky_o <= 1'b0;
break_sticky_o <= 1'b0;
end else begin
if (clear_i) begin
frame_error_sticky_o <= 1'b0;
parity_error_sticky_o <= 1'b0;
overrun_sticky_o <= 1'b0;
break_sticky_o <= 1'b0;
end
// Written AFTER the clear, so the set wins on a collision.
if (frame_error_evt_i) frame_error_sticky_o <= 1'b1;
if (parity_error_evt_i) parity_error_sticky_o <= 1'b1;
if (overrun_evt_i) overrun_sticky_o <= 1'b1;
if (break_evt_i) break_sticky_o <= 1'b1;
end
end
assign any_error_o = frame_error_sticky_o | parity_error_sticky_o
| overrun_sticky_o | break_sticky_o;
endmodule3. The Collision: Clear and Set on the Same Edge
This is the decision the block exists to make, and it is the one most often left to accident.
Software reads the status register, sees an error, and writes a clear. On the very clock edge that clear takes effect, a new error event pulses. Two behaviours are possible and both are implementable in one line:
| Set-dominant | Clear-dominant | |
|---|---|---|
| Result | flag stays set | flag goes clear |
| Loses | nothing | the new event, silently |
| Software sees | the error again on its next read | nothing — as if it never happened |
| Risk | a duplicate report of one error | an error that is never reported at all |
// SET-DOMINANT — the clear is written first, the set after, so the set wins.
if (clear_i) err_q <= 1'b0;
if (error_event_i) err_q <= 1'b1; // later write wins on a collision
// CLEAR-DOMINANT — reverse the order.
if (error_event_i) err_q <= 1'b1;
if (clear_i) err_q <= 1'b0; // later write wins on a collisionThis design is set-dominant, and the argument is asymmetric consequences.
Clear and event on the same edge
8 cycles4. Per-Character Status and Historical Status Are Different
Two consumers want two different things, and a design that provides only one is inadequate for the other.
| Per-character | Historical (sticky) | |
|---|---|---|
| Question | was this byte good? | has anything gone wrong since I looked? |
| Scope | exactly one frame | many frames |
| Travels with | the data | nothing — it is global |
| Built by | Chapter 6.4's publish discipline | this chapter's OR accumulation |
| Consumer | logic deciding what to do with a byte | software polling occasionally |
The per-character form is the primitive; the sticky form is derived from it. Chapter 6.4 §5 argued this and it is worth restating as a direction: given per-frame flags you can build a sticky register by ORing each accepted byte's status into a held bit. Given only a sticky register you cannot recover which byte was bad, because the association was destroyed when several frames were merged into one bit.
So a design that exposes only sticky status has made per-byte decisions impossible for anything downstream. A design that exposes only per-character status forces software to catch every byte, which defeats the purpose of a status register.
Both are cheap and they answer different questions, which is why real UART IP exposes both — and why this module built the per-frame flags first (Module 6) and the sticky ones last.
Where per-character status travels with buffered data, each FIFO entry carries its own flags alongside its byte. That is Module 10's architecture, and it is only possible because the per-character primitive exists.
5. Verification
The collision test is the one that matters and the one that is missing from most suites, because it requires deliberately aligning a clear with an event — something no nominal traffic produces.
From this module's simulation:
| Stimulus | Expected | Result |
|---|---|---|
| event, no clear | sticky set | pass |
| clear, no event | sticky clears | pass |
any_error_o with any flag set | asserted | pass |
| clear and event on the same cycle | sticky STAYS SET | pass |
The fourth row is the architecture under test. Written as a property it is unambiguous, and it fails immediately if someone reorders the two assignments during a refactor — which is exactly the kind of change that looks harmless:
// Assertion — SET-DOMINANT. A colliding clear must not destroy an event.
// This is the policy of §3 made executable; it fails on a reordering.
property p_set_dominant;
@(posedge clk) disable iff (!rst_n)
(clear_i && frame_error_evt_i) |=> frame_error_sticky_o;
endproperty
assert property (p_set_dominant);
// Assertion — a sticky flag never sets without a cause.
property p_sticky_needs_event;
@(posedge clk) disable iff (!rst_n)
$rose(frame_error_sticky_o) |-> $past(frame_error_evt_i);
endproperty
assert property (p_sticky_needs_event);
// Assertion — and never clears without a clear or a reset.
property p_sticky_holds;
@(posedge clk) disable iff (!rst_n)
$fell(frame_error_sticky_o) |-> $past(clear_i);
endproperty
assert property (p_sticky_holds);
// Assertion — the aggregate is exactly the OR of its inputs.
property p_any_error_is_or;
@(posedge clk) disable iff (!rst_n)
any_error_o == (frame_error_sticky_o | parity_error_sticky_o
| overrun_sticky_o | break_sticky_o);
endproperty
assert property (p_any_error_is_or);The third property is the one that catches an accidentally clear-dominant flag under sustained errors, where the flag would appear to flicker rather than hold.
6. Recovery Is a Different Problem
Detection asks "did something violate the frame?". Recovery asks "where is the next trustworthy boundary?". They are independent, and a receiver can be excellent at the first and poor at the second.
The failure mode to avoid is amplification: one malformed frame producing a burst of errors because the receiver kept trying to decode from a position that was never valid.
Chapter 6.2 already made the single most important recovery decision, and it is worth recognising as one:
S_STOPexits unconditionally. A bad stop is reported, not retried.
That one choice is what prevents the worst amplification. A receiver that waited in S_STOP for the line to return to mark would be permanently disabled by a break — never idle, never detecting a candidate, never receiving again even after the line recovered. Leaving unconditionally means the receiver is back in S_IDLE watching, and a line still low simply produces no candidate because there is no mark to leave.
So the base receiver already recovers correctly from a framing error. What it does not do is recover from the cases Chapter 9.5 identified, where the receiver's timing origin is wrong rather than one field.
The recovery model
Three conditions, and they use signals this module has already built:
| Condition | Recovery requirement | Signal |
|---|---|---|
| Framing or parity error | none — report and continue | — |
| Break | wait for release, then for proven idle | break_released_o, line_idle_o |
| Suspected mis-framing | wait for proven idle | line_idle_o |
// Conceptual SystemVerilog — recovery as a gate on start acceptance,
// not as new states inside the receive FSM. Chapter 6.1 §3's argument:
// the frame's state graph must not depend on line history.
//
// The receiver of Module 6 is unchanged; this qualifies its input.
assign may_accept_start = start_cand_i
&& !break_active_o // not during a break
&& (!recovery_armed_q || line_idle_o);Waiting for line_idle_o is the strong form of recovery and it carries Chapter 9.5 §5's cost exactly: on a saturated link it never completes. A design that arms it after every error on a busy link stops receiving. That is why the gate above arms it only for the cases that need it — a break, or a suspected timing-origin failure — and not for an ordinary framing error, which needs no recovery at all.
7. Debugging
8. What This Means on an FPGA
The whole status block is four flip-flops and an OR. There is no version of this that is too expensive, and the collision policy costs nothing at all — it is the order of two statements.
Add counters, not just flags. Four bits per error class turns "something went wrong" into a rate. It is the single highest-value addition to a bring-up build and it is usually absent.
Expose break_active_o separately from break_sticky_o. They answer different questions — is it broken now versus was it ever — and a design that merges them cannot answer the first.
Probe the FSM state alongside the flags. Chapter 6.2 §10 noted that three bits of state make the receiver's behaviour legible; combined with the status set it distinguishes a receiver that is detecting errors from one that has stopped receiving altogether, which the flags alone cannot.
Check that clear_i is a pulse, not a level. A clear held high permanently — easy to produce with a mis-decoded register write — makes every sticky flag permanently clear under set-dominance and permanently unreadable. The assertion in §5 that a flag never falls without a clear will not catch it, because there genuinely is a clear.
9. Understanding Check
10. Summary
Events are one-cycle pulses; software cannot see them. Sticky flags hold them and lose what the event had — count, timing and frame association — which is why a counter is strictly more informative than a flag. break_active_o is a third shape: a condition level that clears itself, because "is it broken now" is a different question from "was it ever".
The collision policy is the decision this block exists to make. When a clear and an event land on the same edge, set-dominance keeps the error and clear-dominance destroys it. The failures are asymmetric: a duplicate report is cosmetic, a lost error defeats the register's purpose. Set-dominant here; clear-dominant where a flag gates behaviour rather than reporting it. Never by accident of statement ordering — the difference appears only in a race no nominal test produces.
Per-character and historical status answer different questions, and the per-character form is the primitive. Sticky is derived from it; the reverse is impossible, because merging frames destroys the association.
Recovery is not detection. The base receiver already recovers from framing and parity errors, because Chapter 6.2 made S_STOP exit unconditionally — the choice that prevents a break from permanently disabling the link.
Recovery belongs outside the frame FSM, as a gate on start acceptance built from the break and idle detectors. That keeps the state graph enumerable and verifiable without line activity — the module's most transferable idea, now made three times: conditions depending on line history belong alongside the frame machine, never inside it.
And the strong recovery has a cost: waiting for proven idle never completes on a saturated link, so it is armed for breaks and suspected mis-framing, not for ordinary errors.
11. Where Module 9 Leaves You
Chapter 9.1 established that a framing error is one sample at one instant and consistent with five different causes. Chapter 9.2 proved by exhaustive enumeration that parity catches every odd-weight corruption and misses every even-weight one, and that a passing check licenses nothing about the data. Chapter 9.3 separated a receive overrun from transmit starvation and showed that a flag records a loss it cannot recover or even count. Chapter 9.4 built the only duration-based detector in the curriculum, with the saturation policy and the +1 width that a power-of-two threshold demands. Chapter 9.5 mapped the disturbance window — 4.34 µs to 86.81 µs — in which a glitch becomes a frame nothing reports, and built the idle detection that closes the mid-frame case at a stated price. This chapter turned all of it into state and defined the response.
The theme, stated once: every status bit in a UART receiver is a statement about an observation, not about the world. A framing error says one sample was low. A parity error says one relation failed. An overrun says a slot was full. None of them says "the data is wrong", and a fabricated frame satisfies all of them. An engineer who holds that distinction reads a status register correctly and knows when to stop trusting it.
12. What Comes Next
Module 10 takes the problem this module could only describe. Chapter 9.3 showed a one-entry holding register overrunning when the consumer is more than one frame time behind, and showed that buffering moves the threshold rather than removing it. Module 10 chooses the depth with the facts that decide it — interrupt latency, polling interval, whether DMA is present — builds the TX and RX FIFOs, adds the thresholds and watermarks that let a consumer be warned before data is lost, and implements the flow control that lets a receiver do what no mechanism in this module could: ask the far end to stop.
After that: Module 11 assembles an IP from both halves, the generator and the buffering; Module 12 returns to the clock-domain and reset questions; Module 13 builds the register interface these flags have been waiting for.
Browse the full path on the UART tutorials index. For the per-frame status this chapter aggregates, read back to Chapter 6.4.
Continue learning
Related tutorials
- Related topic
Parity Check, Stop-Bit Validation and Error Status
The receiver decides whether a frame was good, then faces the harder question: which frame does each status flag describe? Getting that wrong lets an incoming frame rewrite the status of a byte the consumer has not yet read.
- Related topic
Framing Errors
A framing error is one sampled bit at one instant. It proves the line was not at mark where the receiver expected mark — and nothing about why, which is what makes the diagnosis interesting.
- Related topic
Parity Errors and the Limits of Detection
Parity detects every odd-weight corruption and provably misses every even-weight one — shown by exhaustive enumeration. A check that passes says a relation holds, not that the data is correct.
- Related topic
Overrun, Underrun and Data Loss
Overrun is a protocol-level consequence of a full holding register; transmit starvation is an architecture-dependent system event. Both lose information an error flag records but cannot recover.
Where this fits
Part of the UART curriculum.
