UART · Module 11
Loopback, Test Modes and Built-In Self-Check
Internal and line loopback, what each one proves, and — measured — the faults they provably cannot see. The cheapest RTL in the IP and the most decisive during bring-up.
A link that does not work presents as one symptom: nothing arrives. Behind that symptom are at least six candidates — the transmitter, the receiver, the baud generator, the pins, the wiring, and the far end — and a UART with no test modes gives you no way to eliminate any of them.
This chapter builds the features that partition that space. They are the cheapest RTL in the entire IP: two muxes, one extra port. They are also the only thing that turns a bring-up from guessing into bisection.
The discipline that matters most here is knowing what a passing test mode does not prove — so §3 breaks the receive pin and shows internal loopback passing anyway.
1. Two Loopbacks, Facing Opposite Directions
They are routinely confused because both are called "loopback". They test disjoint things.
Internal loopback connects this IP's transmitter to its own receiver, before the pins. It asks: do my engines work?
Line loopback connects the receive pin straight back to the transmit pin. It asks nothing about this IP at all — it lets the far end ask: does the path to that board and back work?
// Both loopbacks, complete. They face opposite directions and are
// independent; setting both is legal and is not a combinational loop,
// because rx_i is an input.
logic tx_line, rx_line;
assign tx_o = cfg_line_loop_i ? rx_i : tx_line;
assign rx_line = cfg_loopback_i ? tx_line : rx_i;That is the entire feature. Two ternaries. Everything else in this chapter is about what they let you conclude.
The transmit pin keeps driving during internal loopback — the mux feeds the receiver a copy of the line, it does not steal it. That is deliberate: the frames stay visible on an oscilloscope, so a self-test can be watched from outside. Measured: internal loopback: tx_o STILL drives the pin (scope-visible) — pass.
2. What Internal Loopback Proves
It exercises, in one measurement, nearly everything Modules 6 through 10 built:
| Covered | Because |
|---|---|
| transmit shift, framing, parity generation | the frame has to be well-formed to decode |
| receive sampling, start detection, parity check | it has to decode that frame |
| the shared timing generator | both halves use the same ticks — Chapter 11.2 §2 |
| both FIFOs and their handshakes | the bytes travel through them |
| the configured frame format end to end | wrong parity shows as a parity error |
The timing generator is the valuable one. A baud rate that is wrong by 10% still round-trips perfectly in internal loopback, because both halves are wrong identically — so loopback does not validate the baud rate. What it validates is that the tick distribution is coherent, which is a different and still useful claim.
3. The Fault Internal Loopback Cannot See
Reasoning about coverage is weaker than measuring it. So: break the receive pin — stuck at 0, a dead pad or a broken trace, entirely outside anything the IP can reach — and run internal loopback anyway.
pass internal loopback: byte returns
pass internal loopback: no errors
pass RX PIN STUCK AT 0 : internal loopback STILL PASSES
-> a dead receive pin is invisible to internal loopbackThe byte round-trips perfectly over a receive pin that is physically dead, because rx_line is sourced from tx_line and never touches the pin. This is the correct behaviour and it is exactly the trap: a passing self-test on a board whose receive path is broken.
Which is precisely why line loopback exists, and why the pair is worth more than either alone:
| internal loopback | line loopback | |
|---|---|---|
| TX engine | ✅ | ❌ |
| RX engine | ✅ | ❌ |
| timing generator | ✅ (coherence only) | ❌ |
| FIFOs | ✅ | ❌ |
| TX pin / pad | ❌ | ✅ |
| RX pin / pad | ❌ | ✅ |
| board wiring, connector, cable | ❌ | ✅ |
| absolute baud rate | ❌ | ❌ |
| far-end configuration | ❌ | ❌ |
The two columns are almost perfectly complementary, which is the reason to build both. Line loopback was measured driving an external frame onto the receive pin with both engines uninvolved: line loopback: tx pin echoes the rx pin — pass.
Neither column covers baud rate or the far end. No test mode can: both are statements about a second device.
4. Reading a Dead Link
With the two modes, bring-up becomes a decision procedure rather than a search:
| Internal | Line | Normal | Conclusion |
|---|---|---|---|
| ✅ | ✅ | ❌ | IP and pins are fine — baud rate or far-end configuration |
| ✅ | ❌ | ❌ | engines fine — pins, pads, or board wiring |
| ❌ | — | ❌ | the IP itself: clock, reset, timing generator, engines |
| ✅ | ✅ | ✅ | working; if data is still wrong, it is format, not plumbing |
Three tests, four conclusions, and each eliminates a region rather than guessing at a cause. The first row is the common one and the reason to check baud first when loopback passes.
5. Test Modes Worth Their Gates, and Ones That Are Not
The two loopbacks are unambiguously worth building. The rest of the usual list is more mixed:
Worth it — a forced break. One bit that holds tx_o low lets you generate a break on demand, which is the only way to test the far end's break handling without unplugging a wire. It costs one OR gate on the transmit output, and Chapter 9.4 §8 specified it.
Worth it — exposing cfg_pending_q. One status bit, and it converts "my configuration write did nothing" into "the change is queued and the link has not gone quiet" — Chapter 11.3 §3.
Worth it — a counter on received bytes and on errors. Cheap, and it turns "it is dropping data sometimes" into a rate.
Usually not worth it — an RTL pattern generator. A walking-ones self-test in hardware duplicates what software can do with a loop over the existing write port, and it must be maintained in RTL where changing it costs a re-synthesis. Build the loopback; let software supply the patterns.
Not worth it — a test mode that changes the datapath. Any mode that alters how data is processed rather than how it is routed creates a configuration in which the IP has never shipped. The two loopbacks here only re-route: every engine, FIFO and status path behaves identically in loopback and in normal operation, which is what makes a loopback result meaningful about normal operation.
6. The Loopback Routing in Three Languages
Two multiplexers, and their independence is the whole design.
// Verilog-2001
wire tx_line, rx_line;
assign tx_o = cfg_line_loop_i ? rx_i : tx_line;
assign rx_line = cfg_loopback_i ? tx_line : rx_i;-- VHDL-2008
tx_o <= rx_i when cfg_line_loop_i = '1' else tx_line;
rx_line <= tx_line when cfg_loopback_i = '1' else rx_i;Both set at once is legal and is not a combinational loop, and the reason
is worth being precise about: rx_i is an input. The line-loopback mux
drives tx_o from a pin the IP does not control, so the path
rx_i → tx_o terminates at a pad. The internal-loopback mux drives
rx_line from tx_line, which the transmitter drives. Neither output feeds
the other's input, so there is no cycle to break.
Swapping the two controls is caught by 9 of 60 checks in Chapter 11.2 §11's mutation table — the loudest defect in the module, because it breaks end-to-end data flow and both loopback tests at once.
7. Verification
Loopback is so useful as a test tool that it is easy to forget it is also logic that must be tested. Four properties matter:
// Assertion — internal loopback must not disturb the transmit pin. The
// receiver takes a COPY; stealing the line would make self-test invisible
// to a scope and would silence the link the moment the mode is entered.
property p_loopback_keeps_tx_pin;
@(posedge clk) disable iff (!rst_n)
(cfg_loopback_i && !cfg_line_loop_i) |-> (tx_o == tx_line);
endproperty
assert property (p_loopback_keeps_tx_pin);
// Assertion — line loopback echoes the pin combinationally, whatever the
// engines are doing.
property p_line_loop_echoes;
@(posedge clk) disable iff (!rst_n)
cfg_line_loop_i |-> (tx_o == rx_i);
endproperty
assert property (p_line_loop_echoes);
// Assertion — with both modes off, neither path is engaged.
property p_no_mode_is_transparent;
@(posedge clk) disable iff (!rst_n)
(!cfg_loopback_i && !cfg_line_loop_i) |-> (tx_o == tx_line && rx_line == rx_i);
endproperty
assert property (p_no_mode_is_transparent);The third is the one that protects production. It states that the test features are absent when not selected, which is the property every test mode must have and the one that a mux with a wrong polarity would break — in a way that only shows up on hardware, never in the loopback tests that were used to develop the feature.
Test that leaving a mode restores normal operation, not only that entering it works. Measured: line loop off: tx_o driven by the transmitter again — pass. A one-directional test passes on a mux that latches.
All three held for the entire run. And they were mutation-checked, because an assertion that has never failed is an assertion that might not be connected to anything: rewiring internal loopback to steal the transmit pin rather than copy it produced 8,680 assertion failures against zero on the real RTL.
The full result for this chapter's suite, including the broken-pin injection:
== 13 checks, 0 failures ==Routing, and what each mode can see
6 cycles8. What This Means on an FPGA
Two 2:1 muxes on pin paths. One extra level of logic between the transmit register and the pad. On any FPGA this is noise against the pad delay itself, and it is worth knowing the level is there when reading a timing report for a fast link.
Bring up with internal loopback before the cable exists. It needs no peer, no external equipment and no correct baud rate — which means it can run on the first day the board powers on, and a pass eliminates the entire IP from the search.
Then check the bit period on a scope. Loopback cannot do it (§2), and it is the most common remaining fault. One measurement on tx_o, compared against Chapter 4.3's table.
Put line loopback behind a documented register bit and tell the far-end team it exists. Its whole value is to somebody debugging from the other end of the cable, and an undocumented test feature helps nobody.
Print the mode bits in the driver's diagnostic output. The failure mode of loopback is being left on, and it is indistinguishable from a broken link until somebody reads the register.
9. Assertions Across the Module
Six properties, and each names an integration defect that leaves the design working:
// p_busy_excludes_storage -- catches busy_o including unread RX data. A
// driver that waits for !busy before sleeping would never sleep.
property p_busy_excludes_storage;
@(posedge clk) disable iff (!rst_n)
(!tx_busy_o && tx_empty_o && !rx_active) |-> !busy_o;
endproperty
// p_config_stable_while_active -- catches link_quiet missing rx_active.
property p_config_stable_while_active;
@(posedge clk) disable iff (!rst_n)
(tx_busy_o || rx_active) |=> $stable(cfg_parity_q);
endproperty
// p_rx_push_is_handshake -- catches a push driven by the offer alone, which
// re-writes the same character on every subsequent pop.
property p_rx_push_is_handshake;
@(posedge clk) disable iff (!rst_n)
rx_push_evt_o |-> (rx_char_valid && rx_char_ready);
endproperty
// p_no_push_when_full -- the FIFO must never be offered a push it cannot
// take, which is what makes its overflow output unreachable here.
property p_no_push_when_full;
@(posedge clk) disable iff (!rst_n) rx_fifo_full |-> !rx_push;
endproperty
// p_rts_permits_when_disabled -- catches a disabled feature driving the
// pin's idle level instead of the permitted level.
property p_rts_permits_when_disabled;
@(posedge clk) disable iff (!rst_n) !cfg_hw_flow_en_i |-> !rts_n_o;
endproperty
// p_loopbacks_independent -- catches the two controls being swapped or
// merged. They face opposite directions and neither implies the other.
property p_loopbacks_independent;
@(posedge clk) disable iff (!rst_n)
(cfg_line_loop_i && !cfg_loopback_i) |-> (tx_o == rx_i);
endproperty| Property | Defect it catches | Why it would otherwise be missed |
|---|---|---|
p_busy_excludes_storage | busy_o includes stored data | the link works; only power management breaks |
p_config_stable_while_active | link_quiet missing rx_active | invisible under loopback, where TX and RX coincide |
p_rx_push_is_handshake | push on the offer alone | only at the RX FIFO's full boundary |
p_no_push_when_full | backpressure removed | data loss under sustained load only |
p_rts_permits_when_disabled | disabled feature drives idle | the far end silently stops sending |
p_loopbacks_independent | the two controls swapped | both are "loopback", so a casual test passes |
10. UVM Mapping
The full environment is Module 16's subject. What is worth fixing here is that an integration agent is a different shape from a block agent, and the difference follows from this module.
| Contract element | UVM component | What it carries |
|---|---|---|
| a byte to transmit | write sequence item | data |
| how fast software drains | read sequence | service_latency, burst_size |
| a configuration change | config sequence item | parity, flow_mode, loopback, flush |
driving rx_i | line driver | a real serial frame, correct or corrupt |
observing tx_o | line monitor | reconstructs frames from the wire |
| observing the read port | port monitor | what the consumer actually received |
| comparison | scoreboard | the two streams, plus status lifetime |
Three things are decided by this module, not by UVM.
There are two independent stimulus streams and they must be separate sequences. A block agent drives one interface; this IP has a write port, a read port, a configuration port and a serial line, and the interesting behaviour lives in their relative timing — a config change during a receive, a flush during a transmit, a slow consumer during a burst. Modelling them as one sequence makes those crossings unreachable.
The scoreboard needs TWO views of the same data. The line monitor sees
what left tx_o; the port monitor sees what arrived at the read port. Under
internal loopback they should match exactly; under line loopback they should
not, because the engines are bypassed. A scoreboard with only one view cannot
tell a working link from a working loopback.
Configuration is a transaction with a deferred effect. §3 of
Chapter 11.3 makes a config
write take effect at an unpredictable later time — whenever the link next
goes quiet. A scoreboard that applies it immediately will mispredict every
frame in flight. It has to model link_quiet itself, which is the one place
an integration scoreboard legitimately mirrors design state.
Coverage worth collecting:
| Axis | Bins | Why |
|---|---|---|
| RX occupancy at arrival | empty, mid, DEPTH−1, DEPTH, DEPTH+1 | the last bin is the holding register |
| config change timing | idle, during TX, during RX, during both | only the third catches link_quiet |
| flush timing | idle, mid-frame, queue full | a flush during a frame is the hard case |
| loopback mode | off, internal, line, both | both-set is legal and rarely tested |
| flow control | none, RTS/CTS, XON/XOFF, both | four different backpressure surfaces |
The cross that earns its keep is config-change-timing × loopback-mode, because internal loopback hides the rx_active term
that the whole commit-on-quiet mechanism depends on.
11. Module Evidence
One top-level IP, in three languages, each with a self-checking integration testbench in its own language, built on twelve sub-blocks that were verified independently in Modules 6–10.
| Design | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
uart_ip integration | 60 | 60 | 60 |
180 checks across three languages, 0 failures.
| Language | Tool | Elaborates | Simulates |
|---|---|---|---|
| Verilog-2001 | Icarus Verilog 13.0 (-g2001) | clean, 11 sub-blocks | yes |
| SystemVerilog | Icarus Verilog 13.0 (-g2012) | clean, 12 sub-blocks | yes |
| VHDL-2008 | NVC 1.23.0 (LLVM 23.1.1) | clean, 11 sub-blocks | yes |
Every sub-block is instantiated unmodified. Nothing in Modules 6–10 was changed to make this integration work, which is the claim an IP assembly has to be able to make and the reason the sub-blocks were given real ports instead of hierarchical references.
The integration mutation campaign
| Mutation | Failures |
|---|---|
| the two loopback controls swapped | 9 / 60 |
| RX push gated on the offer, not the handshake | 4 / 60 |
| receiver never backpressured | 2 / 60 |
busy_o includes unread RX occupancy | 1 / 60 |
| disabled HW flow drives the pin's idle level | 1 / 60 |
link_quiet ignores rx_active | 1 / 60 |
| status aggregates only one overrun source | 0 — proven unreachable |
| break detector fed the raw line | 0 — not observable in simulation |
Five of the six killed mutants are caught by one or two checks, and every one of those checks exists because the property was written down before the code was. Three of the eight defects were found by mutation testing rather than by review — the first version of this testbench scored 0 on M2, M3, M6, M7 and M8, and closing the first three required adding scenarios that no block-level test has any reason to contain: saturating the RX queue, and receiving while the transmitter is idle.
12. Understanding Check
13. Summary
Two ternaries and one extra port buy the ability to partition a dead link instead of guessing at it.
Internal loopback and line loopback face opposite directions and cover almost disjoint regions — engines and timing versus pins and board — which is why the pair is worth far more than either alone.
Neither can check the baud rate, because both halves share one generator and a wrong divisor cancels out. Loopback passing while the link is dead is the most common bring-up result, and it points at baud rate precisely because everything else has been eliminated.
A dead receive pin is invisible to internal loopback — measured, with the pin forced stuck at zero and the byte still returning intact. That is correct behaviour and exactly why the second mode exists.
A stuck-low pin is loud and a stuck-high pin is silent: one garbage byte, a framing error and a standing break, versus a trace indistinguishable from a healthy idle link.
Test modes must only re-route, never re-process, or a loopback result says nothing about normal operation. And the assertion that matters most is that with every mode off the datapath is transparent — the one property nothing else exercises, and the one whose failure ships.
14. What Comes Next
The IP is complete. Eleven blocks assembled, configuration plumbed and deferred, status aggregated, parameters checked at elaboration, and test modes to bring it up — verified by 53 integration checks, three parameter sets and a broken pin.
Everything remaining is about the world it has to live in. Module 12 takes the clock-domain question seriously: this IP assumes one clock, and a real SoC will not offer that — which brings genuine asynchronous FIFOs, reset methodology, timing constraints and synthesis signoff. Module 13 attaches it to a bus, and the register map, interrupts and DMA hooks this module deliberately kept out become the subject.
Browse the full path on the UART tutorials index. For the break behaviour a stuck-low pin triggers, read back to Chapter 9.4.
Continue learning
Related tutorials
- Related topic
The UART Link: TX, RX, Idle and Full Duplex
A UART link is two independent one-way conductors, not one bidirectional bus — which removes arbitration, turnaround and direction control from the design, makes the naming endpoint-relative, and means full duplex guarantees simultaneity and nothing else.
- Related topic
Top-Level UART IP Architecture
Ten independently verified blocks and the interfaces that join them. The top level adds no new function; what it adds is fan-out, configuration distribution, status aggregation and the decisions that only exist once blocks meet.
- Related topic
Integrating TX, RX, Baud Generation and FIFOs
The details that only appear at the top level: one generator feeding both halves, a flush that must not corrupt a frame in flight, and several distinct loss points that must feed one honest overrun flag.
- Related topic
Configuration, Control and Status Plumbing
Routing configuration to both halves and status back, with defined behaviour when a change arrives mid-traffic — because a format applied to one half and not the other is worse than not applying it at all.
Where this fits
Part of the UART curriculum.
