Skip to content
VLSI Mentor

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.

EventState
Exampleframe_error_evt_oframe_error_sticky_o
Shapeone-cycle pulselevel, held
Answersdid something happen on this clock?has something happened since I last looked?
Lifetimeone cycleuntil cleared or reset
Consumerhardware — counters, aggregatorssoftware, or a slow observer
Loses information?noyes — 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  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;
endmodule

3. 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-dominantClear-dominant
Resultflag stays setflag goes clear
Losesnothingthe new event, silently
Software seesthe error again on its next readnothing — as if it never happened
Riska duplicate report of one erroran error that is never reported at all
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 collision

This design is set-dominant, and the argument is asymmetric consequences.

Clear and event on the same edge

8 cycles
A trace of eight fabric clock cycles showing a sticky error flag under two collision policies. An error event pulses at cycle one and both flags set at cycle two. At cycle four a software clear is asserted on the same cycle as a second error event. Under the set-dominant policy the flag remains set, because the set assignment is written after the clear and therefore wins, so software observes the error on its next read. Under the clear-dominant policy the flag goes low at cycle five and the second event is lost permanently, leaving no record anywhere in the design that it occurred.one policy still reports, the other is silentone policy still reports, theother is silentfirst errorfirst errorCLEAR + EVENT collideCLEAR + EVENT collideset-dom keeps it / clear-dom loses itset-dom keeps it /clear-dom loses iterror_evt_iclear_isticky (SET-dom)sticky (CLEAR-dom)t0t1t2t3t4t5t6t7
Figure 1 — the collision, both ways. Columns are FABRIC CLOCK CYCLES. At cycle 4 a clear and a new event land together. The set-dominant flag keeps the error and software sees it on its next read; the clear-dominant flag goes low and the event is gone from the design entirely, with nothing anywhere recording that it occurred.

4. 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-characterHistorical (sticky)
Questionwas this byte good?has anything gone wrong since I looked?
Scopeexactly one framemany frames
Travels withthe datanothing — it is global
Built byChapter 6.4's publish disciplinethis chapter's OR accumulation
Consumerlogic deciding what to do with a bytesoftware 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:

StimulusExpectedResult
event, no clearsticky setpass
clear, no eventsticky clearspass
any_error_o with any flag setassertedpass
clear and event on the same cyclesticky STAYS SETpass

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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_STOP exits 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:

ConditionRecovery requirementSignal
Framing or parity errornone — report and continue
Breakwait for release, then for proven idlebreak_released_o, line_idle_o
Suspected mis-framingwait for proven idleline_idle_o
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
fsm
A two-state recovery gate that runs alongside a UART receive state machine. In the normal state, start candidates are passed through to the receiver unchanged, so framing and parity errors have no effect on it because those conditions require no recovery. A break declaration, or a suspicion that the receiver's timing origin is wrong, moves the gate to an armed state in which start candidates are blocked. The gate returns to normal only when the line has been observed continuously at the mark level for a full frame time, which proves no frame is in progress. Because this machine is separate from the receive state machine, the receive machine's transitions still depend only on its own state, the configuration and the sampling event, so its graph remains enumerable and verifiable without line activity.NORMALARMEDbreak_evt / mis-frame suspectedbreak_evt / mis-frame suspectedbreak_evt /mis-frame…line_idle_o — a full frame of markline_idle_o — a full frame of markline_idle_o — afull frame of…framing / parity error: no effectframing / parityerror: no effectstart candidates BLOCKEDstart candidates BLOCKEDstartcandidates…
Figure 2 — the recovery gate, which is a separate two-state machine running alongside the receive FSM rather than inside it. The receive FSM of Chapter 6.2 is unchanged and still has its eight enumerated transitions; this machine only decides whether a start candidate may be offered to it. Ordinary framing and parity errors do not arm it, because they need no recovery.

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

Where this fits

Part of the UART curriculum.